Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ name: CI
on:
push:
branches: [master, main]
# Every pull request, whatever it targets. A base-branch filter here would
# skip each layer of a stacked PR except the bottom one, which is exactly the
# work that most needs checking before it reaches master.
pull_request:
branches: [master, main]

# Least privilege: jobs only read the repo. Override per-job if more is needed.
permissions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
run: npm run build

# Static Pages has no gateway/nginx to rewrite the fragment asset prefix,
# and /__wf/* is outside the project-site mount. Point the marketplace
# and /__wf/* is outside the project-site mount. Point the knowledge base
# stylesheet at its real served path (/knowledge-base/style.css → dist/style.css).
- name: Rewrite fragment asset paths for static hosting
run: find dist -name '*.html' -exec sed -i 's#/__wf/knowledge-base/#/knowledge-base/#g' {} +
Expand Down
24 changes: 22 additions & 2 deletions .github/workflows/pr-requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
name: PR Requirements

on:
# No base-branch filter: a stacked PR targets the layer below it, and each
# layer needs the same hygiene checks as the one that reaches master.
pull_request:
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
branches: [master]

# Least privilege: read PR metadata only.
permissions:
Expand All @@ -19,6 +20,25 @@ jobs:
name: PR Requirements
runs-on: ubuntu-latest
steps:
# A stacked PR targets the layer below it rather than master, so a fixed
# allow-list of `master` fails every layer but the bottom one. Allow the
# base branch as well when it is itself a convention-named branch — that
# still rejects a PR aimed at some unrelated branch, which is what the
# check is for, while letting a stack be reviewed layer by layer.
- name: Resolve allowed target branches
id: targets
env:
KB_BASE_REF: ${{ github.event.pull_request.base.ref }}
KB_BRANCH_PATTERN: '^(feat|feature|fix|bugfix|hotfix|release|support|chore|docs|ci|test)/[a-zA-Z0-9._/-]+$'
run: |
set -euo pipefail
if printf '%s' "$KB_BASE_REF" | grep -Eq "$KB_BRANCH_PATTERN"; then
echo "Stacked PR: also allowing the layer below ($KB_BASE_REF)."
echo "list=master,$KB_BASE_REF" >> "$GITHUB_OUTPUT"
else
echo "list=master" >> "$GITHUB_OUTPUT"
fi

- name: Check PR requirements
uses: AbsaOSS/check-pr-requirements@adb22f6d88c93fd801e787a63d948d6fbbdd4af3 # v0.2.0
with:
Expand Down Expand Up @@ -48,4 +68,4 @@ jobs:
# segments in dependabot branches (e.g. dependabot/npm_and_yarn/…).
branch-pattern: "^(feat|feature|fix|bugfix|hotfix|release|support|chore|docs|ci|test|dependabot)/[a-zA-Z0-9._/-]+$"
max-files-changed: "50"
allowed-target-branches: "master"
allowed-target-branches: ${{ steps.targets.outputs.list }}
14 changes: 7 additions & 7 deletions .github/workflows/validate-doc-app.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Reusable workflow — validate a doc app against the knowledge-base contract.
#
# Called by doc-app repos in their own CI to verify the app meets the marketplace
# Called by doc-app repos in their own CI to verify the app meets the knowledge base
# contract before raising a PR or publishing a release.
#
# Usage in a doc repo (.github/workflows/validate.yml):
Expand Down Expand Up @@ -28,7 +28,7 @@ permissions:

jobs:
validate:
name: Validate marketplace contract
name: Validate knowledge base contract
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down Expand Up @@ -106,15 +106,15 @@ jobs:
echo "✓ dist/${ENTRY} found"

# ── Validate headless HTML structure ───────────────────────────────────
- name: Check data-mp-headless attribute
- name: Check data-kb-headless attribute
run: |
ENTRY=$(node -e "const m=require('./marketplace.json'); console.log(m.entryPoint || 'index.html')")
if ! grep -q 'data-mp-headless="true"' "dist/${ENTRY}"; then
echo "::error file=dist/${ENTRY}::Missing data-mp-headless=\"true\" on <html> element."
echo "Add data-mp-headless=\"true\" to the <html> tag when building with --headless."
if ! grep -q 'data-kb-headless="true"' "dist/${ENTRY}"; then
echo "::error file=dist/${ENTRY}::Missing data-kb-headless=\"true\" on <html> element."
echo "Add data-kb-headless=\"true\" to the <html> tag when building with --headless."
exit 1
fi
echo "✓ data-mp-headless=\"true\" found"
echo "✓ data-kb-headless=\"true\" found"

- name: Check no fixed site header in headless output
run: |
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@ add a registry entry that requires network or a sibling checkout without it.
### Two build modes, one document

Every page renders through `src/layouts/Base.astro`. Headless (web-fragment) and
standalone differ only by `data-mp-headless` and the shadow-DOM compat styles —
standalone differ only by `data-kb-headless` and the shadow-DOM compat styles —
not by a different layout. Changes that add a mode-specific code path need a
strong reason.

### Light only

The marketplace has no dark mode: no theme toggle, no persisted theme, no `dark`
The knowledge base has no dark mode: no theme toggle, no persisted theme, no `dark`
class, no dark palette. A sub-app's own theme bootstrap is removed twice:
`scripts/hoist-inline-scripts.js` deletes it while it is still inline, and
`src/utils/transform.js` strips any that survives, along with a `dark` body
Expand Down
22 changes: 11 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`.
- `src/pages/index.astro` — Landing catalog page
- `src/utils/apps.js` — `getAppPages()` enumerates sub-app HTML (manifest-driven or filesystem crawl)
- `src/utils/transform.js` — `transformSubAppHtml()`: URL rewriting, document splitting (head/body/title/body-class), headless transforms
- `src/layouts/Base.astro` — The one document shell: head, marketplace CSS (which carries the self-hosted Inter faces), `<ClientRouter />`, shadow-DOM compat styles
- `src/layouts/Base.astro` — The one document shell: head, knowledge base CSS (which carries the self-hosted Inter faces), `<ClientRouter />`, shadow-DOM compat styles
- `src/components/Masthead.astro` — Persistent Knowledge base header + Library/current-app sub-nav (all pages, both modes)
- `src/components/AppCard.astro`, `src/components/AppIcon.astro` — Catalog card and its icon
- `src/templates/shadow-compat.js` — Shadow-DOM design-token styles, injected into the body by the layout
Expand All @@ -90,21 +90,21 @@ An `apps.json` entry is one of:

- **default (packaged)** — a repo publishes a headless static site as `dist.tar.gz` plus `marketplace.json`. Every HTML file becomes a route.
- **`type: "iframe"`** — no artifact; a single route renders a full-viewport `<iframe>` for an external URL. Explicit stopgap (issue #10).
- **`type: "single-page"`** — one release artifact holding *many* docs, published by `actions/publish-single-page-docs` from plain markdown. The entry carries **no per-doc metadata** (`{ "repo": …, "type": "single-page", "version": "latest" }`); the build reads the artifact's `bundle.json` and **expands** the entry into one app per doc, extracting each into `apps/{slug}/`. The expansion is recorded in `apps/.single-page.json` and spliced back into the registry by `loadRegistry()` so Astro sees the same registry the build did. Slugs must be globally unique — `resolveRegistry()` fails the build otherwise. Rendering: masthead, no sidebar, content in a centred `main.mp-single-page` reading column. See issue #35 and `contract/SINGLE_PAGE.md`.
- **`type: "single-page"`** — one release artifact holding *many* docs, published by `actions/publish-single-page-docs` from plain markdown. The entry carries **no per-doc metadata** (`{ "repo": …, "type": "single-page", "version": "latest" }`); the build reads the artifact's `bundle.json` and **expands** the entry into one app per doc, extracting each into `apps/{slug}/`. The expansion is recorded in `apps/.single-page.json` and spliced back into the registry by `loadRegistry()` so Astro sees the same registry the build did. Slugs must be globally unique — `resolveRegistry()` fails the build otherwise. Rendering: masthead, no sidebar, content in a centred `main.kb-single-page` reading column. See issue #35 and `contract/SINGLE_PAGE.md`.

### Two Modes

Both modes render the same document: the masthead (`Masthead.astro`) — branding plus the Library / current-app sub-navigation — on every page, and nothing else chrome-like. There is no fixed top bar and no app switcher; the masthead is the navigation.

**Non-headless** (standalone): Plain marketplace pages. Navigation is Astro's `<ClientRouter />` (view transitions).
**Non-headless** (standalone): Plain knowledge base pages. Navigation is Astro's `<ClientRouter />` (view transitions).

**Headless** (web-fragment): Marks `data-mp-headless="true"` on `<html>`, for embedding in a web-fragments gateway. That attribute is the only difference in the output — the shadow-DOM compat styles are emitted in both modes.
**Headless** (web-fragment): Marks `data-kb-headless="true"` on `<html>`, for embedding in a web-fragments gateway. That attribute is the only difference in the output — the shadow-DOM compat styles are emitted in both modes.

Resolution order: a per-app `"headless"` in `apps.json` wins; otherwise `isHeadlessBuild()`.

### Light Only

The marketplace has no dark mode: no theme toggle, no persisted theme, no `dark` class, no dark palette. A sub-app's own theme bootstrap is removed twice over — `hoist-inline-scripts.js` deletes it while it is still inline, and `transformSubAppHtml()` strips any that reaches Astro, along with a `dark` body class — so an embedding host's theme cannot bleed into the fragment.
The knowledge base has no dark mode: no theme toggle, no persisted theme, no `dark` class, no dark palette. A sub-app's own theme bootstrap is removed twice over — `hoist-inline-scripts.js` deletes it while it is still inline, and `transformSubAppHtml()` strips any that reaches Astro, along with a `dark` body class — so an embedding host's theme cannot bleed into the fragment.

Known gap: inline `on*` handlers in sub-app HTML are not stripped (#67). They are inert under the CSP but not under `astro dev`.

Expand All @@ -118,8 +118,8 @@ Root-relative `url()` inside a sub-app's **copied CSS files** is a separate rewr

Apps registered in `apps.json` must comply with:
- `contract/schema.json` — JSON Schema for `marketplace.json` manifest (packaged apps)
- `contract/HEADLESS_RULES.md` — Structural requirements (headless HTML, relative paths, `data-mp-headless` attribute)
- `contract/STYLE_GUIDE.md` — Design tokens and typography (light only — the marketplace has no dark mode)
- `contract/HEADLESS_RULES.md` — Structural requirements (headless HTML, relative paths, `data-kb-headless` attribute)
- `contract/STYLE_GUIDE.md` — Design tokens and typography (light only — the knowledge base has no dark mode)
- `contract/SINGLE_PAGE.md` — `bundle.json` format + the copy-paste onboarding workflow for single-page docs

## Testing
Expand All @@ -133,15 +133,15 @@ Self-contained Playwright E2E — `npm test` auto-starts everything (no external
`tests/fragment-server.mjs` serves `dist/` mirroring the production **nginx** rewrites
(`/__wf/knowledge-base/*` → `/knowledge-base/*`). NB: `astro preview` is NOT used — its
Vite `configurePreviewServer` rewrite hook does not run for static output, so the
marketplace CSS 404s; the nginx-mirror server is the faithful fragment endpoint.
knowledge base CSS 404s; the nginx-mirror server is the faithful fragment endpoint.
2. **:4201 host** — `tests/host/server.mjs`, a minimal Express "wrapping web-fragment
application" (`FragmentGateway` + `getNodeMiddleware`) that proxies/embeds the :3000
fragment on a single origin via `<web-fragment fragment-id="knowledge-base">`.

Tests drive the host origin (`http://localhost:4201`). Suites (`tests/`), all four
commands listed in `AGENTS.md`:
- `build-integrity.spec.js` — `dist/` output: both apps enumerated, absolute URL rewriting,
headless markup and the per-app `"headless"` override, the content-hashed marketplace
headless markup and the per-app `"headless"` override, the content-hashed knowledge base
stylesheet plus its stable `dist/style.css` alias, no inline script anywhere, and
single-page bundle expansion (`tests/fixtures/single-page-bundle/` → two apps).
- `transform.spec.js` — unit tests for `transformSubAppHtml()`: the malformed and
Expand All @@ -159,7 +159,7 @@ commands listed in `AGENTS.md`:

Two build-pipeline pieces support this: `apps.json` entries may carry a `prebuilt` path
(tarball or dist dir) consumed by `scripts/build-vite.js` (`preparePrebuilt`) for hermetic
offline builds; and the build copies the marketplace stylesheet — identified as the local
offline builds; and the build copies the knowledge base stylesheet — identified as the local
stylesheet the landing page loads — to a stable `dist/style.css` alias. Pages themselves
reference the content-hashed bundle Astro injects, so nothing depends on that filename.

Expand All @@ -172,7 +172,7 @@ in the committed `apps.json` without breaking CI, which only has this repo.
## Environment Variables

- `GITHUB_TOKEN` — GitHub API auth for fetching Release artifacts
- `MP_HEADLESS` — `true` produces web-fragment output; **anything else, including unset, means standalone**. `scripts/build-vite.js` always exports an explicit value, so the default only applies when `astro build`/`astro dev` runs directly. Read it through `isHeadlessBuild()`, never inline. A per-app `"headless"` in `apps.json` overrides it in either direction.
- `KB_HEADLESS` — `true` produces web-fragment output; **anything else, including unset, means standalone**. `scripts/build-vite.js` always exports an explicit value, so the default only applies when `astro build`/`astro dev` runs directly. Read it through `isHeadlessBuild()`, never inline. A per-app `"headless"` in `apps.json` overrides it in either direction.
- `AWS_REGION`, `ECR_REPOSITORY`, `ECS_CLUSTER`, `ECS_SERVICE` — deployment config
- `KB_EXAMPLE_ARTIFACT` — overrides the packaged artifact `scripts/setup-test-apps.mjs` registers
- `KB_CONTAINER_PORT`, `KB_SKIP_BUILD` — container-suite harness (`tests/container/serve.mjs`); CI sets `KB_SKIP_BUILD` because its image job already built the image
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ npm test
| `npm run build:local` | Build each app from a local checkout (`localPath`) |
| `npm run build:local:headless` | Local + headless |

`--headless` (or `MP_HEADLESS=true`) produces fragment-ready output, marked with
`data-mp-headless="true"` on `<html>`. Anything else — including an unset
`MP_HEADLESS` — means standalone. An individual app can pin either mode with
`--headless` (or `KB_HEADLESS=true`) produces fragment-ready output, marked with
`data-kb-headless="true"` on `<html>`. Anything else — including an unset
`KB_HEADLESS` — means standalone. An individual app can pin either mode with
`"headless": true|false` in its `apps.json` entry, which wins over the build flag.

Orchestrator: `scripts/build-vite.js` (flags: `--local`, `--headless`).
Expand Down Expand Up @@ -119,7 +119,7 @@ never quietly produce an empty deployment.
Teams that already host their docs elsewhere and can't yet produce a headless
package can be listed immediately with an **iframe** entry — no `repo`,
`marketplace.json`, or artifact needed. It renders as a full-viewport `<iframe>`
below the marketplace masthead and shows an **External** badge in the catalogue.
below the knowledge base masthead and shows an **External** badge in the catalogue.

```jsonc
{
Expand All @@ -135,7 +135,7 @@ below the marketplace masthead and shows an **External** badge in the catalogue.
```

The external site must permit embedding (its CSP `frame-ancestors` /
`X-Frame-Options` must not block the marketplace origin). See issue #10.
`X-Frame-Options` must not block the knowledge base origin). See issue #10.

### single-page onboarding (markdown, zero config)

Expand Down Expand Up @@ -275,13 +275,13 @@ app.use(getNodeMiddleware(gateway)); // before host static/catch-all r

## Contract for doc apps

Apps must comply with the marketplace contract before they can be registered:
Apps must comply with the knowledge base contract before they can be registered:

| Document | Description |
|---|---|
| [`contract/schema.json`](contract/schema.json) | JSON Schema for `marketplace.json` |
| [`contract/HEADLESS_RULES.md`](contract/HEADLESS_RULES.md) | Headless HTML, relative paths, `data-mp-headless` |
| [`contract/STYLE_GUIDE.md`](contract/STYLE_GUIDE.md) | Design tokens (`--color-kb-*`) and typography — light only; the marketplace has no dark mode |
| [`contract/HEADLESS_RULES.md`](contract/HEADLESS_RULES.md) | Headless HTML, relative paths, `data-kb-headless` |
| [`contract/STYLE_GUIDE.md`](contract/STYLE_GUIDE.md) | Design tokens (`--color-kb-*`) and typography — light only; the knowledge base has no dark mode |
| [`contract/SINGLE_PAGE.md`](contract/SINGLE_PAGE.md) | `bundle.json` format + zero-config markdown onboarding |

> The checklist and workflows below apply to **packaged** doc apps. Single-page
Expand All @@ -290,7 +290,7 @@ Apps must comply with the marketplace contract before they can be registered:
### Checklist
- [ ] `marketplace.json` in repo root, valid against `contract/schema.json`
- [ ] `npm run build -- --headless` produces a headless `dist/`
- [ ] `data-mp-headless="true"` on `<html>` in headless output
- [ ] `data-kb-headless="true"` on `<html>` in headless output
- [ ] No fixed site-level header in headless output
- [ ] All asset paths relative (no leading `/`)
- [ ] GitHub Release tagged `v*` with a `dist.tar.gz` asset
Expand Down Expand Up @@ -369,7 +369,7 @@ knowledge-base/
│ ├── layouts/Base.astro
│ ├── components/ ← AppCard, AppIcon, Masthead
│ ├── templates/shadow-compat.js ← Shadow-DOM design-token styles
│ ├── styles/marketplace.css ← Design tokens + Tailwind
│ ├── styles/knowledge-base.css ← Design tokens + Tailwind
│ └── utils/
│ ├── apps.js ← loadRegistry() + getAppPages() page enumeration
│ ├── config.js ← PATH_PREFIX / BASE_PATH + isHeadlessBuild()
Expand Down
2 changes: 1 addition & 1 deletion actions/publish-single-page-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ npm run selftest

The self-test is deliberately **not** part of the knowledge base's Playwright
suite: that suite must stay hermetic and must not depend on this action's
`node_modules`. The marketplace side of the feature is covered there instead, via
`node_modules`. The knowledge base side of the feature is covered there instead, via
the `tests/fixtures/single-page-bundle/` fixture.
Loading