diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 53e2f25..c7f26c8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,7 +23,7 @@ { "name": "voyage", "source": "./plugins/voyage", - "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, multi-session resumption, session decomposition, and headless execution. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): pencil-toggle annotation mode, select text or click any element, pick intent (Fiks/Endre/Spørsmål), comment, Copy Prompt, paste back, Claude revises the .md." + "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, multi-session resumption, session decomposition, and headless execution. /trekbrief, /trekplan, and /trekreview each end by printing a copy-paste-ready /playground document-critique invocation for the produced artifact — one paste launches an interactive annotation HTML in the browser." }, { "name": "linkedin-thought-leadership", @@ -54,11 +54,6 @@ "name": "human-friendly-style", "source": "./plugins/human-friendly-style", "description": "Shared Claude Code output style for the ktg-plugin-marketplace. Plain-language tone — explains what and why, hides paths/JSON/stack traces by default, matches the user's language." - }, - { - "name": "claude-design", - "source": "./plugins/claude-design", - "description": "End-to-end facilitator for prompting Claude Design (claude.ai/design) — idea to copy-paste-ready prompt with iteration coaching, citing Anthropic primary sources." } ] } diff --git a/CLAUDE.md b/CLAUDE.md index af11fc2..9d2e48f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,13 +10,13 @@ plugins/ config-audit/ v3.1.0 — Configuration intelligence (health, opportunities, auto-fix, whats-active) graceful-handoff/ v2.1.0 — Auto-trigger handoff via Stop hook (skill + JSON pipeline + 4-step model-aware context resolution) linkedin-thought-leadership/ v1.2.0 — LinkedIn content pipeline + analytics - llm-security/ v7.7.2 — Security scanning, auditing, threat modeling. HTML report output for all 18 skill commands (render-report CLI + canonical ESM module mirrored bit-identical into the playground). v7.7.2 translated the remaining Norwegian surface text in the playground UI, the canonical renderer, the agent prompts, and the README/CLAUDE.md state sections to English. v7.7.1 stripped the playground to the catalog as the only routable surface. - ms-ai-architect/ v1.15.0 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command + v3 project-view (sidebar med 17 artifacts + main + import-modal overlay, v2-surface fjernet i v1.15.0) + llm-security/ v6.0.0 — Security scanning, auditing, threat modeling + ms-ai-architect/ v1.13.1 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command okr/ v1.0.0 — OKR guidance for Norwegian public sector - voyage/ v5.0.3 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). /trekbrief, /trekplan, and /trekreview each end by running scripts/annotate.mjs against the just-written .md and printing the file:// link to a self-contained operator-annotation HTML modelled on claude-code-100x/build-site.js: pencil-toggle annotation mode, select text or click any element, choose intent (Fiks/Endre/Spørsmål), comment, sidebar groups by section with delete + Copy Prompt, localStorage persistence per artifact path. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8; v5.0.1 pointed at /playground document-critique (wrong direction); v5.0.2 was operator-led but too thin; v5.0.3 matches the reference the operator pointed at from day one. + voyage/ v5.0.1 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). At the end of /trekbrief, /trekplan, and /trekreview, the operator gets a literal copy-paste-ready `/playground build a document-critique playground for {artifact_path}` invocation — one paste launches an interactive annotation HTML, Copy Prompt button returns the operator notes to Claude, Claude revises the .md. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8; v5.0.1 dropped the redundant standalone HTML render (`render-artifact.mjs`) and made the /playground invocation literal. shared/ - playground-design-system/ v0.6.0 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts. Tier 1 base + Tier 2 + Tier 3 wave 1+2 (20 components) + Tier 4 project-view-arketype (v0.6.0 — sidebar + main + import-modal overlay). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit. + playground-design-system/ v0.1 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts (Tier 1+2+3 wave 1+wave 2 = 20 Tier 3 components total). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit playground-examples/ — Reference scenarios (ROS-Lier, OKR-Bærum, security-Direktorat) + showcase landing + 12 isolated Tier 3 wave 2 component demos under components/ ``` @@ -53,20 +53,3 @@ Disse trackes IKKE i git. Oppdater ved sesjonsslutt. 3. Les REMEMBER.md og TODO.md for sesjonsstatus 4. Jobb innenfor scope 5. Oppdater REMEMBER.md ved avslutning - -## Communication patterns - -### Linking to local files - -When pointing to local files in responses, always use markdown link syntax with a descriptive name: - -- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks ``. -- Always use absolute paths. Never `~/` or relative paths. -- For multiple files, render as a bullet list of named markdown links. - -Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner. - -Example: - -- [Brief](file:///Users/ktg/.../brief.html) -- [Research summary](file:///Users/ktg/.../research/summary.md) diff --git a/README.md b/README.md index 0f4df4e..a2911de 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Then open Claude Code and type `/plugin` to browse and install plugins from the ## Plugins -### [LLM Security](plugins/llm-security/) `v7.7.2` +### [LLM Security](plugins/llm-security/) `v7.6.1` Security scanning, auditing, and threat modeling for agentic AI projects. @@ -36,12 +36,9 @@ Built on OWASP LLM Top 10 (2025), OWASP Agentic AI Top 10, and the AI Agent Trap - **Deterministic scanning** — 23 Node.js scanners (10 orchestrated + 13 standalone) for byte-level analysis: Shannon entropy, Unicode codepoints, typosquatting detection, taint flow, DNS resolution, git forensics, AI-BOM, attack simulation, IDE extension prescan (VS Code + JetBrains — URL fetch from Marketplace / OpenVSX / direct VSIX / JetBrains Marketplace, hardened ZIP extractor for zip-slip / symlinks / bombs, plus OS sandbox via `sandbox-exec` / `bwrap` so the kernel enforces FS confinement), MCP cumulative-drift baseline reset (E14 — sticky baseline catches slow-burn rug-pulls). Bash-normalize T1-T6 for obfuscation-resistant denylists - **Advisory analysis** — 20 commands that scan, audit, and model threats with structured reports, letter grades, and actionable remediation - **Enterprise governance** — Compliance mapping (EU AI Act, NIST AI RMF, ISO 42001), SARIF 2.1.0 output, structured audit trail, policy-as-code, standalone CLI -- **v7.7.2 language consistency pass (2026-05-19)** — Norwegian had crept into surface text across v7.5-v7.7. Per the `~/.claude/CLAUDE.md` convention (English for code and documentation, Norwegian for dialog only), this release translates the HTML Report-step in all 18 skill commands, the canonical CLI renderer `scripts/lib/report-renderers.mjs`, the playground UI strings, the skill-scanner and mcp-scanner agent prompts, the marketplace + plugin README/CLAUDE.md state sections, and six table cells in `docs/scanner-reference.md`. Demo-state fixture content for the `dft-komplett-demo` project (intentional Norwegian persona) and regex alternations that match Norwegian-language report markdown (`/^high\|^høy/`, `/resolution\|løsning/`) were preserved. No scanner, hook, or behavior changes — purely surface text -- **v7.7.1 playground UX strip (2026-05-18)** — Operator feedback immediately after v7.7.0: the catalog became the only routable surface in the playground (the onboarding/home/project render functions remain in source but are not routable). Topbar simplified to a `Catalog` button + state/theme actions. Breadcrumb org-name replaced with a neutral `llm-security`. The onboarding concept (per-command context injection) is documented as a v7.8.0 candidate in ROADMAP. No scanner or hook behavior changes -- **v7.7.0 HTML report for all 18 skill commands (2026-05-18)** — Every `/security ` that produces a report now prints a clickable `file://` link to a self-contained HTML version. Delivered across five sessions: (1) playground catalog list-view + builder-pane with a copy button; (2) playground project-surface cleanup (stub-screen + topbar split); (3) the 18 inline parsers + renderers moved to a canonical ESM module `scripts/lib/report-renderers.mjs` (the playground keeps a bit-identical inline copy since ESM `import` does not work from `file://`); (4) new zero-dep CLI `scripts/render-report.mjs` — stdin/file/stdout mode, kebab→camel commandId routing, inlines 6 DS stylesheets, ~140 KB self-contained HTML with system-font fallback, absolute `file://` paths for Ghostty cmd-click; (5) all 18 skills wired (4 in session 4 + 14 in session 5). No scanner or hook behavior changes — purely additive -- **v7.6.1 playground visual patch (2026-05-06)** — Six bugs caught by the maintainer during manual browser verification after the v7.6.0 release. All were mismatches between DS classes and how playground renderers used them (or missing DS implementations the renderers assumed existed): `renderFindingsBlock` used the `.findings` outer class (the DS 2-column list+detail grid) → replaced with `
` + the correct `findings__list` pattern; `.report-table` was missing entirely from the DS but used in 7+ renderers → local CSS implementation; `renderPreDeploy` traffic-lights used the fixed 28×28 px `.sm-card__grade` for "PASS"/"PASS-WITH-NOTES"/"FAIL" → width-adapting status pill; threat-model matrix bubbles were not clickable → `\n' - + ' \n' - + ' \n' - + '\n' - + '
\n' - + '
Click any heading, paragraph, list item, table cell, or quote to add an annotation. To anchor on a specific phrase, select the text first, then click. Toggle annotation mode off (pencil button) to read normally / follow links.
\n' - + '
\n' - + articleHtml - + '\n
\n' - + '
\n' - + '\n' - + '\n' - + '
\n' - + '
\n' - + '\n' - + '\n' - + '\n'; -} - -// --------------------------------------------------------------------------- -// Stylesheet — light + dark + print. Design-system-aligned. -// --------------------------------------------------------------------------- - -const STYLE = ` -:root { - --bg: #f7f7f8; - --bg-elev: #ffffff; - --bg-soft: #ececef; - --border: #d6d8dc; - --border-strong: #b3b7bd; - --text: #1a1a1a; - --text-dim: #555a63; - --text-mute: #8a8f97; - --accent: #0855a8; - --accent-soft: #e4ecf6; - --amber: #a86b00; - --amber-soft: #fbeed1; - --green: #1a7f37; - --green-soft: #d5ecdb; - --red: #b3262d; - --red-soft: #f6d9da; - --blue: #0855a8; - --blue-soft: #e4ecf6; - --orange: #d4790a; - --orange-soft: #fceede; - --purple: #6638b6; - --purple-soft: #ebe1f9; - --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; - --sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif; - --serif: ui-serif, "Source Serif 4", Georgia, "Times New Roman", serif; -} -@media (prefers-color-scheme: dark) { - :root { - --bg: #0e1218; - --bg-elev: #161b22; - --bg-soft: #1c232c; - --border: #2a323c; - --border-strong: #3b4554; - --text: #e5e9ef; - --text-dim: #a5adba; - --text-mute: #6e7681; - --accent: #6db0ee; - --accent-soft: rgba(109, 176, 238, 0.15); - --amber: #d4a017; - --amber-soft: rgba(212, 160, 23, 0.12); - --green: #3fb950; - --green-soft: rgba(63, 185, 80, 0.12); - --red: #f0626a; - --red-soft: rgba(240, 98, 106, 0.12); - --blue: #6db0ee; - --blue-soft: rgba(109, 176, 238, 0.15); - --orange: #f6ad55; - --orange-soft: rgba(246, 173, 85, 0.15); - --purple: #d2a8ff; - --purple-soft: rgba(210, 168, 255, 0.15); - } -} -* { box-sizing: border-box; } -html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); - font-family: var(--sans); font-size: 15px; line-height: 1.6; } -body { min-height: 100vh; } -/* Topbar */ -.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; justify-content: space-between; - gap: 16px; padding: 12px 24px; background: var(--bg-elev); border-bottom: 1px solid var(--border); } -.hdr-meta h1 { font-size: 16px; font-weight: 650; margin: 0; } -.hdr-meta .path { color: var(--text-dim); font-size: 12px; font-family: var(--mono); margin: 2px 0 0; word-break: break-all; } -.hdr-actions { display: flex; gap: 8px; align-items: center; } -.ann-toggle { display: inline-flex; align-items: center; gap: 6px; - background: var(--accent); color: #fff; border: 1px solid var(--accent); - border-radius: 5px; padding: 6px 12px; font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; } -.ann-toggle:hover { filter: brightness(1.05); } -body:not(.ann-mode) .ann-toggle { background: var(--bg-soft); color: var(--text-dim); border-color: var(--border); } -body:not(.ann-mode) .ann-toggle:hover { color: var(--text); border-color: var(--border-strong); } -.ann-badge { background: rgba(255,255,255,0.25); color: inherit; padding: 0 6px; border-radius: 99px; font-size: 11px; font-weight: 700; } -body:not(.ann-mode) .ann-badge { background: var(--bg); color: var(--text-dim); } -.ghost-btn { background: transparent; color: var(--text-dim); border: 1px solid var(--border); - border-radius: 5px; padding: 6px 12px; font-family: inherit; font-size: 13px; cursor: pointer; } -.ghost-btn:hover { color: var(--text); border-color: var(--border-strong); } -.icon-btn { background: transparent; border: none; color: var(--text-dim); cursor: pointer; - font-size: 16px; padding: 4px 8px; border-radius: 4px; } -.icon-btn:hover { color: var(--text); background: var(--bg-soft); } -/* Article */ -.article-wrap { max-width: 820px; margin: 0 auto; padding: 24px 32px 96px; } -.article-help { font-size: 13px; color: var(--text-dim); background: var(--accent-soft); - border: 1px solid var(--accent); border-radius: 6px; padding: 10px 14px; margin: 0 0 24px; line-height: 1.5; } -body:not(.ann-mode) .article-help { display: none; } -.article-help strong { color: var(--text); } -.article { font-size: 15px; line-height: 1.7; } -.article h1, .article h2, .article h3, .article h4, .article h5, .article h6 { - font-family: var(--serif); font-weight: 700; line-height: 1.25; margin: 1.8em 0 .55em; color: var(--text); } -.article h1 { font-size: 2rem; margin-top: 0; } -.article h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: .3em; } -.article h3 { font-size: 1.2rem; } -.article h4 { font-size: 1.05rem; } -.article p { margin: .9em 0; } -.article a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } -.article code { font-family: var(--mono); font-size: .9em; background: var(--bg-soft); - padding: .12em .4em; border-radius: 4px; } -.article pre { position: relative; background: #1e1e24; color: #e6e6eb; padding: 16px 18px; border-radius: 8px; - overflow-x: auto; font-size: .88rem; line-height: 1.55; margin: 1.2em 0; } -.article pre code { background: none; padding: 0; color: inherit; font-size: inherit; } -.code-copy-btn { position: absolute; top: 8px; right: 8px; background: rgba(255,255,255,0.08); - color: #e6e6eb; border: 1px solid rgba(255,255,255,0.15); border-radius: 5px; - padding: 4px 10px; font: 600 11px var(--sans); cursor: pointer; opacity: 0; - transition: opacity .15s ease, background .15s ease; letter-spacing: .02em; z-index: 2; } -.article pre:hover .code-copy-btn, -.article pre:focus-within .code-copy-btn { opacity: 1; } -.code-copy-btn:hover { background: rgba(255,255,255,0.16); } -.code-copy-btn:focus { opacity: 1; outline: 2px solid var(--accent); outline-offset: 2px; } -.code-copy-btn.copied { background: var(--green); color: #fff; border-color: var(--green); } -.article blockquote { margin: 1.2em 0; padding: .5em 1.2em; border-left: 4px solid var(--accent); - background: var(--accent-soft); color: var(--text-dim); border-radius: 0 6px 6px 0; } -.article ul, .article ol { padding-left: 1.8em; margin: .9em 0; } -.article li { margin: .3em 0; } -.article table { border-collapse: collapse; width: 100%; margin: 1.4em 0; font-size: .92em; } -.article th, .article td { border: 1px solid var(--border); padding: .55em .8em; text-align: left; vertical-align: top; } -.article th { background: var(--bg-soft); font-weight: 650; } -.article hr { border: none; border-top: 1px solid var(--border); margin: 2.2em 0; } -.article strong { font-weight: 700; } -.article em { font-style: italic; } -/* Annotation mode: highlight annotatable elements on hover, mark annotated ones */ -.article [data-anchor-id] { position: relative; transition: background .08s, outline .08s; border-radius: 3px; } -body.ann-mode .article [data-anchor-id] { cursor: pointer; } -body.ann-mode .article [data-anchor-id]:hover { - outline: 2px dashed var(--accent); outline-offset: 2px; background: var(--accent-soft); -} -.article [data-anchor-id].annotated { - background: var(--amber-soft); - outline: 1px solid var(--amber); outline-offset: 1px; -} -.article [data-anchor-id].annotated::after { - content: attr(data-ann-count); position: absolute; right: -22px; top: 2px; - background: var(--amber); color: #fff; font-size: 10px; font-weight: 700; - padding: 1px 6px; border-radius: 99px; font-family: var(--sans); -} -body.ann-mode .article [data-anchor-id].annotated:hover { outline-color: var(--amber); } -.article [data-anchor-id].flash { - animation: flash 1.6s ease-out; -} -@keyframes flash { - 0% { background: var(--accent-soft); outline: 2px solid var(--accent); } - 100% { background: var(--amber-soft); outline: 1px solid var(--amber); } -} -/* Form popover */ -.ann-form { position: fixed; z-index: 200; background: var(--bg-elev); border: 1px solid var(--border-strong); - border-radius: 8px; padding: 14px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); - width: 380px; max-width: calc(100vw - 24px); display: none; flex-direction: column; gap: 10px; - font-family: var(--sans); } -.ann-form.visible { display: flex; } -.ann-form-section-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; - color: var(--text-mute); font-weight: 600; margin-bottom: 3px; } -.ann-form-section-value { font-size: 13px; color: var(--text-dim); font-style: italic; } -.ann-form-snippet-text { margin: 0; padding: 6px 10px; border-left: 3px solid var(--accent); - background: var(--bg); border-radius: 0 4px 4px 0; font-family: var(--mono); font-size: 12px; - color: var(--text); max-height: 100px; overflow-y: auto; line-height: 1.5; white-space: pre-wrap; word-break: break-word; } -.ann-form-intents { display: flex; gap: 6px; } -.ann-intent { flex: 1; padding: 7px 10px; border-radius: 5px; border: 1px solid var(--border); - background: transparent; color: var(--text-dim); font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer; } -.ann-intent:hover { color: var(--text); border-color: var(--border-strong); } -.ann-intent[data-intent="fiks"].selected { background: var(--red); color: #fff; border-color: var(--red); } -.ann-intent[data-intent="endre"].selected { background: var(--orange); color: #fff; border-color: var(--orange); } -.ann-intent[data-intent="spørsmål"].selected { background: var(--blue); color: #fff; border-color: var(--blue); } -.ann-form-comment { width: 100%; min-height: 80px; padding: 8px 10px; - font-family: inherit; font-size: 13px; line-height: 1.5; color: var(--text); - background: var(--bg); border: 1px solid var(--border); border-radius: 5px; resize: vertical; } -.ann-form-comment:focus { outline: 1px solid var(--accent); border-color: var(--accent); } -.ann-form-actions { display: flex; gap: 6px; justify-content: flex-end; } -.btn { padding: 6px 14px; border-radius: 5px; border: 1px solid var(--border); - background: transparent; color: var(--text-dim); font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer; } -.btn:hover { color: var(--text); border-color: var(--border-strong); } -.btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); } -.btn.primary:hover:not(:disabled) { filter: brightness(1.1); color: #fff; } -.btn.primary:disabled { background: var(--bg-soft); color: var(--text-mute); border-color: var(--border); cursor: not-allowed; filter: none; } -/* Annotations panel (slide-in sidebar) */ -.ann-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 100vw; - background: var(--bg-elev); border-left: 1px solid var(--border); z-index: 150; - transform: translateX(100%); transition: transform .2s ease; - display: flex; flex-direction: column; box-shadow: -4px 0 20px rgba(0,0,0,0.15); } -.ann-panel.open { transform: translateX(0); } -.ann-panel-head { display: flex; align-items: center; justify-content: space-between; - padding: 14px 18px; border-bottom: 1px solid var(--border); } -.ann-panel-head h2 { font-size: 14px; font-weight: 650; margin: 0; } -.ann-panel-body { flex: 1; overflow-y: auto; padding: 12px 14px; } -.ann-panel-foot { display: flex; justify-content: space-between; gap: 8px; - padding: 12px 14px; border-top: 1px solid var(--border); } -.ann-panel-empty { color: var(--text-mute); font-size: 13px; text-align: center; padding: 32px 12px; - font-style: italic; line-height: 1.5; } -.ann-section { margin: 12px 0 6px; font-size: 11px; font-weight: 650; text-transform: uppercase; - letter-spacing: 0.04em; color: var(--text-mute); padding: 0 4px; } -.ann-section:first-child { margin-top: 0; } -.ann-item { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; - padding: 10px 12px; margin-bottom: 8px; cursor: pointer; } -.ann-item:hover { border-color: var(--border-strong); } -.ann-item .ann-item-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; gap: 6px; } -.ann-item-intent { font-size: 10px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.04em; padding: 2px 8px; border-radius: 99px; } -.ann-item-intent.fiks { background: var(--red-soft); color: var(--red); } -.ann-item-intent.endre { background: var(--orange-soft); color: var(--orange); } -.ann-item-intent.spørsmål { background: var(--blue-soft); color: var(--blue); } -.ann-item-delete { background: transparent; border: none; color: var(--text-mute); - cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 13px; } -.ann-item-delete:hover { color: var(--red); background: var(--red-soft); } -.ann-item-snippet { font-family: var(--mono); font-size: 11px; color: var(--text-mute); - margin: 0 0 6px; line-height: 1.5; padding: 4px 8px; background: var(--bg-soft); - border-left: 2px solid var(--border-strong); border-radius: 0 4px 4px 0; - max-height: 60px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; } -.ann-item-comment { font-size: 13px; color: var(--text); line-height: 1.5; white-space: pre-wrap; word-break: break-word; } -.ann-item-comment.empty { color: var(--text-mute); font-style: italic; } -/* Toast */ -.ann-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px); - background: var(--text); color: var(--bg-elev); padding: 9px 16px; border-radius: 6px; - font-size: 13px; font-weight: 500; opacity: 0; pointer-events: none; - transition: opacity .2s, transform .2s; z-index: 300; } -.ann-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); } -/* Overlay (form backdrop) */ -.ann-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 100; - opacity: 0; pointer-events: none; transition: opacity .15s; } -.ann-overlay.visible { opacity: 1; pointer-events: auto; } -/* Scrollbar */ -::-webkit-scrollbar { width: 10px; height: 10px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 6px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-mute); } -/* Print: hide annotation chrome, show article only */ -@media print { - .topbar, .ann-form, .ann-panel, .ann-toast, .ann-overlay, .article-help { display: none !important; } - .article-wrap { max-width: none; padding: 0; } - body { background: #fff; color: #000; } -} -`.trim(); - -// --------------------------------------------------------------------------- -// Embedded JS app. Uses concatenation (no template literals) to avoid -// backtick collisions with the outer mjs string assembly. -// --------------------------------------------------------------------------- - -const APP_JS = ` -const STORAGE_KEY = 'voyage-annotate:v2:' + ARTIFACT_PATH; -const INTENT_LABELS = { fiks: 'Fiks', endre: 'Endre', 'spørsmål': 'Spørsmål' }; -const INTENT_ORDER = ['fiks', 'endre', 'spørsmål']; - -let annotations = []; -let nextId = 1; -let mode = true; -let currentTarget = null; -let currentSection = null; -let currentSnippet = null; -let currentIntent = null; - -// ── Storage ── -function loadState() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return; - const data = JSON.parse(raw); - if (data && Array.isArray(data.annotations)) { - annotations = data.annotations; - nextId = data.nextId || (annotations.reduce(function(m, a){return Math.max(m, a.id);}, 0) + 1); - } - } catch (e) {} -} -function saveState() { - try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ annotations: annotations, nextId: nextId })); } catch (e) {} -} -function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } - -// ── DOM refs ── -const body = document.body; -const article = document.getElementById('article'); -const form = document.getElementById('ann-form'); -const formSection = document.getElementById('ann-form-section'); -const formSnippet = document.getElementById('ann-form-snippet'); -const formComment = document.getElementById('ann-form-comment'); -const formSave = document.getElementById('ann-form-save'); -const formCancel = document.getElementById('ann-form-cancel'); -const intents = document.querySelectorAll('.ann-intent'); -const panel = document.getElementById('ann-panel'); -const panelBody = document.getElementById('ann-panel-body'); -const panelCloseBtn = document.getElementById('ann-panel-close'); -const openPanelBtn = document.getElementById('open-panel'); -const clearAllBtn = document.getElementById('ann-clear-all'); -const copyBtn = document.getElementById('ann-copy'); -const annToggle = document.getElementById('ann-toggle'); -const annToggleLabel = document.getElementById('ann-toggle-label'); -const annBadge = document.getElementById('ann-badge'); -const toast = document.getElementById('ann-toast'); -const overlay = document.getElementById('ann-overlay'); - -// ── Section lookup ── -function findSection(el) { - let p = el; - while (p) { - if (p.previousElementSibling) { - let s = p.previousElementSibling; - while (s) { - if (s.matches && (s.matches('h1') || s.matches('h2'))) return s.textContent.trim(); - s = s.previousElementSibling; - } - } - p = p.parentElement; - if (p && p.tagName === 'ARTICLE') break; - } - // Fallback: first h1 in article - const firstH = article.querySelector('h1, h2'); - return firstH ? firstH.textContent.trim() : '(top)'; -} - -// ── Snippet from selection or element text ── -function captureSnippet(el) { - const sel = window.getSelection(); - if (sel && sel.toString().trim().length > 0 && el.contains(sel.anchorNode)) { - return sel.toString().trim().slice(0, 300); - } - return (el.textContent || '').trim().slice(0, 200); -} - -// ── Form open/close ── -function openForm(evt, target) { - currentTarget = target; - currentSection = findSection(target); - currentSnippet = captureSnippet(target); - currentIntent = null; - formSection.textContent = currentSection || '(top)'; - formSnippet.textContent = currentSnippet || '(empty)'; - formComment.value = ''; - intents.forEach(function(b) { b.classList.remove('selected'); }); - formSave.disabled = true; - - // Position near the click (clamped to viewport) - const fw = 380, fh = 320; - let x = evt.clientX + 14; - let y = evt.clientY + 14; - if (x + fw > window.innerWidth) x = window.innerWidth - fw - 12; - if (y + fh > window.innerHeight) y = Math.max(12, window.innerHeight - fh - 12); - if (x < 12) x = 12; - if (y < 12) y = 12; - form.style.left = x + 'px'; - form.style.top = y + 'px'; - form.classList.add('visible'); - overlay.classList.add('visible'); - setTimeout(function() { formComment.focus(); }, 50); -} -function closeForm() { - form.classList.remove('visible'); - overlay.classList.remove('visible'); - currentTarget = null; - currentSection = null; - currentSnippet = null; - currentIntent = null; -} - -// ── Save ── -function saveAnnotation() { - if (!currentIntent || !currentTarget) return; - const a = { - id: nextId++, - anchorId: currentTarget.getAttribute('data-anchor-id'), - section: currentSection || '(top)', - snippet: currentSnippet || '', - intent: currentIntent, - comment: (formComment.value || '').trim(), - ts: new Date().toISOString(), - }; - annotations.push(a); - saveState(); - closeForm(); - refreshArticleAnnotations(); - renderPanel(); - updateCounts(); - showToast('Annotasjon lagret (' + annotations.length + ')'); -} - -// ── Delete ── -function deleteAnnotation(id) { - annotations = annotations.filter(function(a) { return a.id !== id; }); - saveState(); - refreshArticleAnnotations(); - renderPanel(); - updateCounts(); - showToast('Annotasjon slettet'); -} - -// ── Refresh article markers ── -function refreshArticleAnnotations() { - // Clear all current markers - article.querySelectorAll('[data-anchor-id].annotated').forEach(function(el) { - el.classList.remove('annotated'); - el.removeAttribute('data-ann-count'); - }); - // Group by anchorId - const byAnchor = {}; - for (const a of annotations) { - if (!a.anchorId) continue; - if (!byAnchor[a.anchorId]) byAnchor[a.anchorId] = 0; - byAnchor[a.anchorId]++; - } - for (const anchorId in byAnchor) { - const el = article.querySelector('[data-anchor-id="' + CSS.escape(anchorId) + '"]'); - if (el) { - el.classList.add('annotated'); - el.setAttribute('data-ann-count', byAnchor[anchorId]); - } - } -} - -// ── Sidebar panel render ── -function renderPanel() { - if (annotations.length === 0) { - panelBody.innerHTML = '
No annotations yet.

Click any heading, paragraph, list item, or quote in the article to add one.
'; - return; - } - // Group by section (preserve insertion order) - const groups = []; - const groupMap = {}; - // Sort by document order using anchorId numerical suffix - const sorted = annotations.slice().sort(function(a, b) { - const ai = parseInt((a.anchorId || '').replace('anch-', ''), 10) || 0; - const bi = parseInt((b.anchorId || '').replace('anch-', ''), 10) || 0; - if (ai !== bi) return ai - bi; - return a.id - b.id; - }); - for (const a of sorted) { - if (!groupMap[a.section]) { - groupMap[a.section] = { section: a.section, items: [] }; - groups.push(groupMap[a.section]); - } - groupMap[a.section].items.push(a); - } - let html = ''; - for (const g of groups) { - html += '
' + escHtml(g.section) + '
'; - for (const a of g.items) { - html += '
' - + '
' - + '' + escHtml(INTENT_LABELS[a.intent] || a.intent) + '' - + '' - + '
' - + '
' + escHtml(a.snippet || '(empty)') + '
' - + '
' + escHtml(a.comment || '(no comment)') + '
' - + '
'; - } - } - panelBody.innerHTML = html; - - panelBody.querySelectorAll('.ann-item-delete').forEach(function(b) { - b.addEventListener('click', function(e) { - e.stopPropagation(); - if (confirm('Delete this annotation?')) deleteAnnotation(parseInt(b.dataset.del, 10)); - }); - }); - panelBody.querySelectorAll('.ann-item').forEach(function(card) { - card.addEventListener('click', function() { - const anchor = card.getAttribute('data-anchor-id'); - const el = article.querySelector('[data-anchor-id="' + CSS.escape(anchor) + '"]'); - if (el) { - el.scrollIntoView({ behavior: 'smooth', block: 'center' }); - el.classList.remove('flash'); - void el.offsetWidth; - el.classList.add('flash'); - } - }); - }); -} - -// ── Counts + toggle label ── -function updateCounts() { - annBadge.textContent = String(annotations.length); - copyBtn.disabled = annotations.length === 0; -} - -function setMode(on) { - mode = on; - body.classList.toggle('ann-mode', on); - annToggleLabel.textContent = on ? 'Annotation mode: ON' : 'Annotation mode: OFF'; - if (!on) closeForm(); -} - -// ── Toast ── -function showToast(msg) { - toast.textContent = msg; - toast.classList.add('visible'); - setTimeout(function() { toast.classList.remove('visible'); }, 1800); -} - -// ── Copy Prompt ── -function buildPromptMarkdown() { - if (annotations.length === 0) return ''; - const sorted = annotations.slice().sort(function(a, b) { - const ai = parseInt((a.anchorId || '').replace('anch-', ''), 10) || 0; - const bi = parseInt((b.anchorId || '').replace('anch-', ''), 10) || 0; - if (ai !== bi) return ai - bi; - return a.id - b.id; - }); - let p = 'Please revise the voyage artifact at \\\`' + ARTIFACT_PATH + '\\\` with the operator annotations below.\\n'; - p += 'Each annotation has an intent — **Fiks** (something is wrong / fix it), **Endre** (change wording/content),\\n'; - p += 'or **Spørsmål** (operator question — clarify or answer). The quote shows what the operator anchored to.\\n'; - p += 'Treat the operator notes as authoritative direction.\\n\\n'; - p += '## Annotations (' + annotations.length + ' total)\\n\\n'; - let n = 0; - for (const a of sorted) { - n++; - p += '### ' + n + '. [' + (INTENT_LABELS[a.intent] || a.intent) + '] Section: ' + a.section + '\\n'; - if (a.snippet) p += 'Quote: «' + a.snippet + '»\\n'; - p += 'Comment: ' + (a.comment || '(no comment)') + '\\n\\n'; - } - return p; -} - -async function copyPrompt() { - const md = buildPromptMarkdown(); - if (!md) return; - try { - await navigator.clipboard.writeText(md); - showToast('Prompt copied (' + annotations.length + ' annotation' + (annotations.length === 1 ? '' : 's') + ')'); - } catch (e) { - // Fallback - const ta = document.createElement('textarea'); - ta.value = md; ta.style.position = 'fixed'; ta.style.opacity = '0'; - document.body.appendChild(ta); ta.select(); - try { document.execCommand('copy'); showToast('Prompt copied'); } catch (e2) { alert('Copy failed: ' + e2.message); } - ta.remove(); - } -} - -// ── Wiring ── -article.addEventListener('click', function(e) { - if (!mode) return; - const target = e.target.closest('[data-anchor-id]'); - if (!target) return; - // Don't open form when clicking inside an already-open form (overlay catches outside clicks) - if (e.target.closest('.ann-form')) return; - // Don't open form when clicking a link the user wants to follow — but only if they didn't select text - if (e.target.tagName === 'A' && (!window.getSelection() || window.getSelection().toString().trim().length === 0)) { - // Allow link clicks in mode if no selection - return; - } - e.preventDefault(); - openForm(e, target); -}); - -intents.forEach(function(b) { - b.addEventListener('click', function() { - intents.forEach(function(x) { x.classList.remove('selected'); }); - b.classList.add('selected'); - currentIntent = b.dataset.intent; - formSave.disabled = false; - }); -}); - -formSave.addEventListener('click', saveAnnotation); -formCancel.addEventListener('click', closeForm); -overlay.addEventListener('click', closeForm); - -formComment.addEventListener('keydown', function(e) { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !formSave.disabled) { - saveAnnotation(); - } else if (e.key === 'Escape') { - closeForm(); - } -}); - -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape' && form.classList.contains('visible')) closeForm(); -}); - -annToggle.addEventListener('click', function() { setMode(!mode); }); - -openPanelBtn.addEventListener('click', function() { - panel.classList.toggle('open'); -}); -panelCloseBtn.addEventListener('click', function() { panel.classList.remove('open'); }); - -clearAllBtn.addEventListener('click', function() { - if (annotations.length === 0) return; - if (confirm('Remove all ' + annotations.length + ' annotations? This cannot be undone.')) { - annotations = []; - saveState(); - refreshArticleAnnotations(); - renderPanel(); - updateCounts(); - showToast('All annotations cleared'); - } -}); - -copyBtn.addEventListener('click', copyPrompt); - -// ── Init ── -loadState(); -refreshArticleAnnotations(); -renderPanel(); -updateCounts(); -setMode(true); - -// ── Code-block copy buttons ── -document.querySelectorAll('.article pre').forEach(function(pre) { - var btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'code-copy-btn'; - btn.textContent = 'Copy'; - btn.setAttribute('aria-label', 'Copy code block to clipboard'); - btn.addEventListener('click', function(e) { - e.stopPropagation(); - var code = pre.querySelector('code'); - var text = code ? code.textContent : pre.textContent; - navigator.clipboard.writeText(text).then(function() { - btn.textContent = 'Copied'; - btn.classList.add('copied'); - setTimeout(function() { - btn.textContent = 'Copy'; - btn.classList.remove('copied'); - }, 1500); - }).catch(function() { - btn.textContent = 'Failed'; - setTimeout(function() { btn.textContent = 'Copy'; }, 1500); - }); - }); - pre.appendChild(btn); -}); -`.trim(); - -// --------------------------------------------------------------------------- -// CLI -// --------------------------------------------------------------------------- - -function parseArgs(argv) { - const args = { input: null, out: null, help: false }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === '--out') args.out = argv[++i]; - else if (a === '--help' || a === '-h') args.help = true; - else if (!args.input) args.input = a; - } - return args; -} - -function render(inputPath, outputPath) { - if (!existsSync(inputPath)) { - process.stderr.write('annotate: input not found: ' + inputPath + '\n'); - process.exit(2); - } - const text = readFileSync(inputPath, 'utf-8'); - const html = buildHtml(resolve(inputPath), text); - const out = outputPath || inputPath.replace(/\.md$/, '.html'); - writeFileSync(out, html); - return out; -} - -if (import.meta.url === `file://${process.argv[1]}`) { - const args = parseArgs(process.argv.slice(2)); - if (args.help || !args.input) { - process.stdout.write( - 'Usage: annotate [--out ]\n\n' - + 'Builds a self-contained operator-annotation HTML for a voyage\n' - + 'artifact. The operator opens the HTML, selects text or clicks any\n' - + 'element, picks an intent (Fiks / Endre / Spørsmål), writes a\n' - + 'comment, and copies a structured prompt to paste back into Claude.\n' - + 'Annotations persist in localStorage per artifact path.\n\n' - + 'Default output: .html next to input.\n', - ); - process.exit(args.help ? 0 : 2); - } - const out = render(args.input, args.out); - process.stdout.write(out + '\n'); -} - -export { render, buildHtml, renderMarkdown, parseArgs }; diff --git a/plugins/voyage/templates/trekbrief-template.md b/plugins/voyage/templates/trekbrief-template.md index e0c2232..b35d893 100644 --- a/plugins/voyage/templates/trekbrief-template.md +++ b/plugins/voyage/templates/trekbrief-template.md @@ -1,6 +1,6 @@ --- type: trekbrief -brief_version: "2.1" +brief_version: 2.0 created: {YYYY-MM-DD} task: "{one-line task description}" slug: {slug} @@ -10,20 +10,6 @@ research_status: pending # pending | in_progress | complete | skipped auto_research: false # true if user opted into Claude-managed research interview_turns: {N} source: {interview | manual} -# v5.1 — per-phase effort + model signal (Phase 3.5). -# `effort` ∈ {low, standard, high}. Omit `model:` for `standard` so composition -# falls through to profile resolver. Force-stop alternative is the commented -# `phase_signals_partial: true` below (mutually exclusive with `phase_signals`). -phase_signals: - - phase: research - effort: standard - - phase: plan - effort: standard - - phase: execute - effort: standard - - phase: review - effort: standard -# phase_signals_partial: true # uncomment to record force-stop instead of phase_signals --- # Task: {title} diff --git a/plugins/voyage/tests/commands/trekbrief.test.mjs b/plugins/voyage/tests/commands/trekbrief.test.mjs deleted file mode 100644 index 0788f67..0000000 --- a/plugins/voyage/tests/commands/trekbrief.test.mjs +++ /dev/null @@ -1,130 +0,0 @@ -// tests/commands/trekbrief.test.mjs -// v5.1 prose-pin tests + v5.1.1 runtime SC1 tests. -// -// Pattern D prose-pins kept as doc-anchors for the .md file. Runtime tests -// added per finding 350853 (BLOCKER SC1) + a7f4f95a (MAJOR Plan Step 5 drift). -// -// SC1 re-interpretation (per plan Step 10 amendment): "asserts on 4 -// AskUserQuestion calls" → "asserts resolvePhaseSignal returns non-null for -// all 4 entries in PHASE_SIGNAL_PHASES when applied to a brief with a -// committed phase_signals block." See brief amendment for full rationale. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; -import { validateBriefContent, PHASE_SIGNAL_PHASES, EFFORT_LEVELS } from '../../lib/validators/brief-validator.mjs'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const COMMAND_FILE = join(ROOT, 'commands', 'trekbrief.md'); -const FIXTURE = (name) => join(ROOT, 'tests', 'fixtures', name); - -function read() { - return readFileSync(COMMAND_FILE, 'utf8'); -} - -function readFixture(name) { - return readFileSync(FIXTURE(name), 'utf8'); -} - -function frontmatterOf(text) { - const doc = parseDocument(text); - return doc.parsed && doc.parsed.frontmatter; -} - -// --- Pattern D prose-pins (doc-anchors) --- - -test('trekbrief — Phase 3.5 heading is present', () => { - const text = read(); - assert.match(text, /^## Phase 3\.5 — Per-phase effort dialog$/m, - 'Phase 3.5 heading missing from commands/trekbrief.md'); -}); - -test('trekbrief — Phase 3.5 references all 4 downstream phases', () => { - const text = read(); - const startIdx = text.indexOf('## Phase 3.5'); - assert.ok(startIdx >= 0, 'Phase 3.5 not found'); - const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx)); - for (const phase of ['research', 'plan', 'execute', 'review']) { - assert.ok(section.includes(phase), - `Phase 3.5 missing reference to "${phase}"`); - } -}); - -test('trekbrief — Phase 3.5 documents phase_signals_partial force-stop', () => { - const text = read(); - assert.ok(text.includes('phase_signals_partial'), - 'phase_signals_partial not mentioned in /trekbrief command prose'); -}); - -// --- v5.1.1 runtime SC1 tests --- - -test('trekbrief — SC1: resolvePhaseSignal returns non-null for all 4 phases on committed brief (brief-effort-low)', () => { - const fm = frontmatterOf(readFixture('brief-effort-low.md')); - for (const phase of PHASE_SIGNAL_PHASES) { - const r = resolvePhaseSignal(fm, phase); - assert.ok(r && typeof r === 'object', - `phase=${phase}: resolver must return non-null for committed brief; got ${JSON.stringify(r)}`); - assert.ok(typeof r.effort === 'string', - `phase=${phase}: resolver result must include effort`); - } -}); - -test('trekbrief — SC1: each of 4 phases has both effort AND model on full-signals fixture', () => { - const fm = frontmatterOf(readFixture('brief-with-phase-signals.md')); - for (const phase of PHASE_SIGNAL_PHASES) { - const r = resolvePhaseSignal(fm, phase); - assert.ok(r && typeof r === 'object', `phase=${phase}: must resolve`); - assert.ok(EFFORT_LEVELS.includes(r.effort), - `phase=${phase}: effort "${r.effort}" not in EFFORT_LEVELS`); - if ('model' in r) { - assert.ok(['sonnet', 'opus'].includes(r.model), - `phase=${phase}: model "${r.model}" not in [sonnet, opus]`); - } - } -}); - -test('trekbrief — SC1: missing phase_signals + brief_version 2.1 triggers BRIEF_V51_MISSING_SIGNALS', () => { - const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true }); - assert.equal(r.valid, false); - assert.ok( - r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), - `gate must fire; errors=${JSON.stringify(r.errors)}`, - ); -}); - -test('trekbrief — SC1: phase_signals_partial: true does NOT trigger the gate', () => { - const partial = `--- -type: trekbrief -brief_version: "2.1" -created: 2026-05-14 -task: "Partial brief" -slug: partial-brief -project_dir: .claude/projects/2026-05-14-partial-brief/ -research_topics: 0 -research_status: complete -auto_research: false -interview_turns: 2 -source: fixture -phase_signals_partial: true ---- - -# Task - -## Intent -Stop early. - -## Goal -Test partial mode. - -## Success Criteria -- gate does not fire. -`; - const r = validateBriefContent(partial, { strict: true }); - assert.equal(r.valid, true, `errors=${JSON.stringify(r.errors)}`); - assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); -}); diff --git a/plugins/voyage/tests/commands/trekexecute.test.mjs b/plugins/voyage/tests/commands/trekexecute.test.mjs deleted file mode 100644 index 15a67c7..0000000 --- a/plugins/voyage/tests/commands/trekexecute.test.mjs +++ /dev/null @@ -1,75 +0,0 @@ -// tests/commands/trekexecute.test.mjs -// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekexecute. -// Plan Assumption 2 locks low-effort to --gates open + sequential-only. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; -import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const COMMAND_FILE = join(ROOT, 'commands', 'trekexecute.md'); -const PHASE = 'execute'; - -function read() { return readFileSync(COMMAND_FILE, 'utf8'); } -function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); } -function frontmatterOf(text) { - const doc = parseDocument(text); - return doc.parsed && doc.parsed.frontmatter; -} - -// --- Pattern D prose-pins --- - -test('trekexecute — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => { - const text = read(); - assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'), - '/trekexecute must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate'); - assert.ok(text.includes('phase_signals'), - '/trekexecute must reference phase_signals (v5.1 composition rule)'); -}); - -test('trekexecute — low-effort path references --gates open + sequential', () => { - const text = read(); - const compIdx = text.indexOf('## Composition rule (v5.1)'); - assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing'); - const section = text.slice(compIdx, compIdx + 2000); - assert.match(section, /--gates open/, 'Low-effort path must mention --gates open'); - assert.match(section, /sequential/, 'Low-effort path must mention sequential-only execution'); -}); - -// --- v5.1.1 runtime SC4 + SC7 --- - -test('trekexecute — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => { - const fm = frontmatterOf(readFixture('brief-effort-low.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'low'); - assert.equal(r.model, 'sonnet'); -}); - -test('trekexecute — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => { - const fm = frontmatterOf(readFixture('brief-effort-standard.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'standard'); - assert.equal(r.model, undefined); -}); - -test('trekexecute — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => { - const fm = frontmatterOf(readFixture('brief-effort-high.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'high'); - assert.equal(r.model, 'opus'); -}); - -test('trekexecute — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => { - const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true }); - assert.equal(r.valid, false); - assert.ok( - r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), - `sequencing gate must fire; errors=${JSON.stringify(r.errors)}`, - ); -}); diff --git a/plugins/voyage/tests/commands/trekplan.test.mjs b/plugins/voyage/tests/commands/trekplan.test.mjs deleted file mode 100644 index 1ab3ee0..0000000 --- a/plugins/voyage/tests/commands/trekplan.test.mjs +++ /dev/null @@ -1,73 +0,0 @@ -// tests/commands/trekplan.test.mjs -// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekplan. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; -import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const COMMAND_FILE = join(ROOT, 'commands', 'trekplan.md'); -const PHASE = 'plan'; - -function read() { return readFileSync(COMMAND_FILE, 'utf8'); } -function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); } -function frontmatterOf(text) { - const doc = parseDocument(text); - return doc.parsed && doc.parsed.frontmatter; -} - -// --- Pattern D prose-pins (kept) --- - -test('trekplan — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => { - const text = read(); - assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'), - '/trekplan must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate'); - assert.ok(text.includes('phase_signals'), - '/trekplan must reference phase_signals (v5.1 composition rule)'); -}); - -test('trekplan — low-effort path references --quick equivalent', () => { - const text = read(); - const compIdx = text.indexOf('## Composition rule (v5.1)'); - assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing'); - const section = text.slice(compIdx, compIdx + 2000); - assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent'); -}); - -// --- v5.1.1 runtime SC4 + SC7 tests --- - -test('trekplan — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => { - const fm = frontmatterOf(readFixture('brief-effort-low.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'low'); - assert.equal(r.model, 'sonnet'); -}); - -test('trekplan — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => { - const fm = frontmatterOf(readFixture('brief-effort-standard.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'standard'); - assert.equal(r.model, undefined); -}); - -test('trekplan — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => { - const fm = frontmatterOf(readFixture('brief-effort-high.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'high'); - assert.equal(r.model, 'opus'); -}); - -test('trekplan — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => { - const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true }); - assert.equal(r.valid, false); - assert.ok( - r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), - `sequencing gate must fire; errors=${JSON.stringify(r.errors)}`, - ); -}); diff --git a/plugins/voyage/tests/commands/trekresearch.test.mjs b/plugins/voyage/tests/commands/trekresearch.test.mjs deleted file mode 100644 index 0e10351..0000000 --- a/plugins/voyage/tests/commands/trekresearch.test.mjs +++ /dev/null @@ -1,73 +0,0 @@ -// tests/commands/trekresearch.test.mjs -// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekresearch. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; -import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const COMMAND_FILE = join(ROOT, 'commands', 'trekresearch.md'); -const PHASE = 'research'; - -function read() { return readFileSync(COMMAND_FILE, 'utf8'); } -function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); } -function frontmatterOf(text) { - const doc = parseDocument(text); - return doc.parsed && doc.parsed.frontmatter; -} - -// --- Pattern D prose-pins --- - -test('trekresearch — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => { - const text = read(); - assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'), - '/trekresearch must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate'); - assert.ok(text.includes('phase_signals'), - '/trekresearch must reference phase_signals (v5.1 composition rule)'); -}); - -test('trekresearch — low-effort path references --quick equivalent', () => { - const text = read(); - const compIdx = text.indexOf('## Composition rule (v5.1)'); - assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing'); - const section = text.slice(compIdx, compIdx + 2000); - assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent'); -}); - -// --- v5.1.1 runtime SC4 + SC7 --- - -test('trekresearch — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => { - const fm = frontmatterOf(readFixture('brief-effort-low.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'low'); - assert.equal(r.model, 'sonnet'); -}); - -test('trekresearch — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => { - const fm = frontmatterOf(readFixture('brief-effort-standard.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'standard'); - assert.equal(r.model, undefined); -}); - -test('trekresearch — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => { - const fm = frontmatterOf(readFixture('brief-effort-high.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'high'); - assert.equal(r.model, 'opus'); -}); - -test('trekresearch — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => { - const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true }); - assert.equal(r.valid, false); - assert.ok( - r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), - `sequencing gate must fire; errors=${JSON.stringify(r.errors)}`, - ); -}); diff --git a/plugins/voyage/tests/commands/trekreview.test.mjs b/plugins/voyage/tests/commands/trekreview.test.mjs deleted file mode 100644 index e66ef00..0000000 --- a/plugins/voyage/tests/commands/trekreview.test.mjs +++ /dev/null @@ -1,74 +0,0 @@ -// tests/commands/trekreview.test.mjs -// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekreview. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; -import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const COMMAND_FILE = join(ROOT, 'commands', 'trekreview.md'); -const PHASE = 'review'; - -function read() { return readFileSync(COMMAND_FILE, 'utf8'); } -function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); } -function frontmatterOf(text) { - const doc = parseDocument(text); - return doc.parsed && doc.parsed.frontmatter; -} - -// --- Pattern D prose-pins --- - -test('trekreview — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => { - const text = read(); - assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'), - '/trekreview must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate'); - assert.ok(text.includes('phase_signals'), - '/trekreview must reference phase_signals (v5.1 composition rule)'); -}); - -test('trekreview — low-effort path references --quick equivalent', () => { - const text = read(); - const compIdx = text.indexOf('## Composition rule (v5.1)'); - assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing'); - const section = text.slice(compIdx, compIdx + 2000); - assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent'); -}); - -// --- v5.1.1 runtime SC4 + SC7 --- - -test('trekreview — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => { - const fm = frontmatterOf(readFixture('brief-effort-low.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'low'); - assert.equal(r.model, 'sonnet'); -}); - -test('trekreview — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => { - const fm = frontmatterOf(readFixture('brief-effort-standard.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'standard'); - assert.equal(r.model, undefined); -}); - -test('trekreview — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => { - const fm = frontmatterOf(readFixture('brief-effort-high.md')); - const r = resolvePhaseSignal(fm, PHASE); - assert.equal(r.effort, 'high'); - assert.equal(r.model, 'opus'); -}); - -test('trekreview — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => { - // Falsification via brief-v21-no-signals fixture: validator must catch missing signals. - const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true }); - assert.equal(r.valid, false); - assert.ok( - r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), - `sequencing gate must fire; errors=${JSON.stringify(r.errors)}`, - ); -}); diff --git a/plugins/voyage/tests/fixtures/brief-effort-high.md b/plugins/voyage/tests/fixtures/brief-effort-high.md deleted file mode 100644 index 0d119b1..0000000 --- a/plugins/voyage/tests/fixtures/brief-effort-high.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -type: trekbrief -brief_version: "2.1" -created: 2026-05-14 -task: "Fixture: high-effort all phases (v5.1.1 runtime test)" -slug: brief-effort-high -project_dir: .claude/projects/2026-05-14-brief-effort-high/ -research_topics: 0 -research_status: complete -auto_research: false -interview_turns: 4 -source: fixture -phase_signals: - - phase: research - effort: high - model: opus - - phase: plan - effort: high - model: opus - - phase: execute - effort: high - model: opus - - phase: review - effort: high - model: opus ---- - -# Task: High-effort fixture - -## Intent - -Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the -high effort tier with explicit opus model overrides. Mirrors the -production-grade premium-profile scenario. - -## Goal - -Resolver returns `{effort: 'high', model: 'opus'}` for each of the 4 -PHASE_SIGNAL_PHASES. - -## Success Criteria - -- Validator passes. -- resolvePhaseSignal(fm, phase).effort === 'high' for all 4 phases. -- resolvePhaseSignal(fm, phase).model === 'opus' for all 4 phases. diff --git a/plugins/voyage/tests/fixtures/brief-effort-low.md b/plugins/voyage/tests/fixtures/brief-effort-low.md deleted file mode 100644 index 40b4f93..0000000 --- a/plugins/voyage/tests/fixtures/brief-effort-low.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -type: trekbrief -brief_version: "2.1" -created: 2026-05-14 -task: "Fixture: low-effort all phases (v5.1.1 runtime test)" -slug: brief-effort-low -project_dir: .claude/projects/2026-05-14-brief-effort-low/ -research_topics: 0 -research_status: complete -auto_research: false -interview_turns: 4 -source: fixture -phase_signals: - - phase: research - effort: low - model: sonnet - - phase: plan - effort: low - model: sonnet - - phase: execute - effort: low - model: sonnet - - phase: review - effort: low - model: sonnet ---- - -# Task: Low-effort fixture - -## Intent - -Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the lowest -effort tier with explicit sonnet model overrides. - -## Goal - -Resolver returns `{effort: 'low', model: 'sonnet'}` for each of the 4 -PHASE_SIGNAL_PHASES. - -## Success Criteria - -- Validator passes. -- resolvePhaseSignal(fm, phase) is non-null for all 4 phases. diff --git a/plugins/voyage/tests/fixtures/brief-effort-standard.md b/plugins/voyage/tests/fixtures/brief-effort-standard.md deleted file mode 100644 index f0bb3dd..0000000 --- a/plugins/voyage/tests/fixtures/brief-effort-standard.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -type: trekbrief -brief_version: "2.1" -created: 2026-05-14 -task: "Fixture: standard-effort all phases, no model (v5.1.1 runtime test)" -slug: brief-effort-standard -project_dir: .claude/projects/2026-05-14-brief-effort-standard/ -research_topics: 0 -research_status: complete -auto_research: false -interview_turns: 4 -source: fixture -phase_signals: - - phase: research - effort: standard - - phase: plan - effort: standard - - phase: execute - effort: standard - - phase: review - effort: standard ---- - -# Task: Standard-effort fixture (no model override) - -## Intent - -Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the -standard tier WITHOUT explicit model fields. This is the operator-skipped -model path that should fall through to the profile. - -## Goal - -Resolver returns `{effort: 'standard', model: undefined}` for each of the 4 -PHASE_SIGNAL_PHASES. The orchestrator-model path then falls through to the -active profile's phase_models. - -## Success Criteria - -- Validator passes. -- resolvePhaseSignal(fm, phase).model is undefined. -- resolvePhaseSignal(fm, phase).effort is 'standard'. diff --git a/plugins/voyage/tests/fixtures/brief-v21-no-signals.md b/plugins/voyage/tests/fixtures/brief-v21-no-signals.md deleted file mode 100644 index d705406..0000000 --- a/plugins/voyage/tests/fixtures/brief-v21-no-signals.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -type: trekbrief -brief_version: "2.1" -created: 2026-05-14 -task: "Fixture: v5.1 brief WITHOUT phase_signals or partial (falsification target)" -slug: brief-v21-no-signals -project_dir: .claude/projects/2026-05-14-brief-v21-no-signals/ -research_topics: 0 -research_status: complete -auto_research: false -interview_turns: 4 -source: fixture ---- - -# Task: brief_version 2.1 without phase_signals - -## Intent - -Falsification fixture for the v5.1 sequencing gate. The brief declares -`brief_version: "2.1"` but omits BOTH `phase_signals` AND -`phase_signals_partial: true`. The brief-validator MUST emit -`BRIEF_V51_MISSING_SIGNALS` for this file — the runtime test for the -sequencing gate asserts the error code fires. - -## Goal - -Validate that brief-validator catches the missing-signals scenario. - -## Success Criteria - -- brief-validator returns valid: false. -- errors contains BRIEF_V51_MISSING_SIGNALS. diff --git a/plugins/voyage/tests/fixtures/brief-with-phase-signals.md b/plugins/voyage/tests/fixtures/brief-with-phase-signals.md deleted file mode 100644 index c68e37c..0000000 --- a/plugins/voyage/tests/fixtures/brief-with-phase-signals.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -type: trekbrief -brief_version: "2.1" -created: 2026-05-13 -task: "Add per-phase effort dialog to /trekbrief" -slug: phase-signals-example -project_dir: .claude/projects/2026-05-13-phase-signals-example/ -research_topics: 2 -research_status: complete -auto_research: false -interview_turns: 6 -source: interview -phase_signals: - - phase: research - effort: low - model: sonnet - - phase: plan - effort: standard - - phase: execute - effort: high - model: opus - - phase: review - effort: standard ---- - -# Task: Phase-signals example - -## Intent - -A minimal brief that exercises the v5.1 phase_signals additive field with a -mix of effort levels and model overrides. Used by tests/validators to confirm -the validator accepts well-formed signals across the supported tier matrix. - -## Goal - -Validator returns valid: true. annotate.mjs strips phase_signals from the -rendered HTML body (frontmatter stays in source). - -## Success Criteria - -- Validator passes. -- annotate.mjs determinism: re-run produces byte-identical HTML. diff --git a/plugins/voyage/tests/fixtures/brief-without-phase-signals.md b/plugins/voyage/tests/fixtures/brief-without-phase-signals.md deleted file mode 100644 index 8bec99e..0000000 --- a/plugins/voyage/tests/fixtures/brief-without-phase-signals.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -type: trekbrief -brief_version: "2.0" -created: 2026-05-13 -task: "Backward-compat fixture for v5.0-style brief" -slug: legacy-brief-example -project_dir: .claude/projects/2026-05-13-legacy-brief-example/ -research_topics: 0 -research_status: complete -auto_research: false -interview_turns: 3 -source: interview ---- - -# Task: Legacy brief example - -## Intent - -A pre-v5.1 brief that pre-dates the phase_signals field. Used by -tests/validators to confirm backward-compatibility: the brief is accepted -without phase_signals as long as brief_version is < 2.1. - -## Goal - -Validator returns valid: true. The sequencing gate -(BRIEF_V51_MISSING_SIGNALS) does NOT fire for brief_version 2.0. - -## Success Criteria - -- Validator passes. -- No BRIEF_V51_MISSING_SIGNALS error in r.errors. diff --git a/plugins/voyage/tests/lib/doc-consistency.test.mjs b/plugins/voyage/tests/lib/doc-consistency.test.mjs index bc96ab4..cbc1c81 100644 --- a/plugins/voyage/tests/lib/doc-consistency.test.mjs +++ b/plugins/voyage/tests/lib/doc-consistency.test.mjs @@ -430,85 +430,42 @@ test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)', assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain'); }); -test('scripts/render-artifact.mjs is still removed (v5.0.1 + v5.0.2)', () => { +test('scripts/render-artifact.mjs no longer exists (removed in v5.0.1)', () => { assert.ok( !existsSync(join(ROOT, 'scripts/render-artifact.mjs')), - 'scripts/render-artifact.mjs should be deleted — v5.0.1 dropped the standalone HTML render; v5.0.2 kept it removed (annotate.mjs is the replacement)', + 'scripts/render-artifact.mjs should be deleted — v5.0.1 drops the redundant standalone HTML render in favour of the /playground document-critique invocation printed by the producing commands', ); }); -test('scripts/annotate.mjs exists (v5.0.2 operator-annotation HTML generator)', () => { - assert.ok( - existsSync(join(ROOT, 'scripts/annotate.mjs')), - 'scripts/annotate.mjs is required — producing commands call it to build the operator-annotation HTML', - ); -}); - -test('producing commands reference scripts/annotate.mjs (v5.0.2 render-and-link step)', () => { - // v5.0.0 → v5.0.1 → v5.0.2 chain: v5.0.0 added an HTML render that didn't - // afford annotation; v5.0.1 pointed at /playground document-critique (which - // pre-generates Claude's suggestions, not operator-driven annotation); v5.0.2 - // ships scripts/annotate.mjs — an operator-driven annotation surface where - // the OPERATOR clicks lines and writes their own notes. Pin the wiring. +test('producing commands print a literal /playground document-critique invocation', () => { + // The exact substring must appear in each producing command's prose so the + // operator copy-pastes a verbatim line. Drift on this is the friction point + // that motivated v5.0.1 — fail loudly if the prose softens back to "run the + // /playground plugin" without the literal command. + const REQUIRED = '/playground build a document-critique playground for'; for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) { assert.ok( - read(`commands/${f}`).includes('scripts/annotate.mjs'), - `commands/${f} must invoke scripts/annotate.mjs to build the operator-annotation HTML (v5.0.2)`, + read(`commands/${f}`).includes(REQUIRED), + `commands/${f} must include the literal invocation "${REQUIRED}" so the operator copy-pastes it directly (v5.0.1)`, ); } }); -test('producing commands no longer print the v5.0.1 /playground document-critique line', () => { - // v5.0.1 told operators to copy-paste "/playground build a document-critique - // playground for X" — but that flow pre-generates Claude's suggestions. The - // operator asked for their own annotations, not a critique of Claude's. - // v5.0.2 removes that line from the producing commands' final report. +test('producing commands no longer reference the removed scripts/render-artifact.mjs', () => { for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) { assert.ok( - !read(`commands/${f}`).includes('/playground build a document-critique'), - `commands/${f} must not print the v5.0.1 /playground document-critique invocation — v5.0.2 replaces it with annotate.mjs`, + !read(`commands/${f}`).includes('render-artifact.mjs'), + `commands/${f} still references scripts/render-artifact.mjs — that script was removed in v5.0.1`, ); } }); -test('producing commands tell the operator the flow is THEIR own annotations', () => { - // Pin language: every producing command's prose must mention that the - // OPERATOR drives annotation, not Claude. Phrase variants are allowed - // ("YOUR OWN note", "operator drives", etc.) — we look for the operator- - // ownership signal. - for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) { - const text = read(`commands/${f}`); - assert.ok( - /YOUR OWN|operator drives|your own/i.test(text), - `commands/${f} must signal that the operator drives annotation (v5.0.2 contract)`, - ); - } -}); - -test('producing commands emit file:// link in final report (operator-UX contract, 2026-05-13)', () => { - // Operator runs Ghostty / iTerm2 / modern Terminal.app — all support cmd+click - // on file:// URLs. Producing commands MUST emit both forms: (a) plain file:// - // line in the report block, (b) `open file://...` copy-pasteable command. - // Both must reference $ANNOT_HTML (absolute path from scripts/annotate.mjs). - for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) { - const text = read(`commands/${f}`); - assert.ok( - /file:\/\/\{\$ANNOT_HTML\}/.test(text), - `commands/${f} must include "file://{$ANNOT_HTML}" plain URL in the final report block`, - ); - assert.ok( - /open file:\/\/\{\$ANNOT_HTML\}/.test(text), - `commands/${f} must include "open file://{$ANNOT_HTML}" copy-pasteable command in the final report block`, - ); - } -}); - -test('package.json still has no "npm run render" script (removed in v5.0.1)', () => { +test('package.json no longer has an "npm run render" script (removed in v5.0.1)', () => { const pkg = JSON.parse(read('package.json')); assert.equal( pkg.scripts && pkg.scripts.render, undefined, - 'package.json scripts.render should remain gone', + 'package.json scripts.render should be gone in v5.0.1', ); }); @@ -522,11 +479,6 @@ test('CHANGELOG.md has v5.0.1 entry', () => { assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry'); }); -test('CHANGELOG.md has v5.0.2 entry', () => { - const cl = read('CHANGELOG.md'); - assert.match(cl, /## v5\.0\.2\b/, 'CHANGELOG.md must include "## v5.0.2" entry'); -}); - test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => { const cl = read('CHANGELOG.md'); assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry'); @@ -551,63 +503,3 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () => ); } }); - -// --- v5.1 — phase_signals + brief_version 2.1 --- - -test('v5.1 — templates/trekbrief-template.md declares brief_version: "2.1" (quoted)', () => { - const t = read('templates/trekbrief-template.md'); - assert.match(t, /^brief_version: "2\.1"$/m, - 'trekbrief-template.md must declare brief_version: "2.1" (quoted) — unquoted parses as Number and bypasses sequencing gate'); -}); - -test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => { - const t = read('templates/trekbrief-template.md'); - assert.match(t, /^phase_signals:$/m, - 'trekbrief-template.md must contain a phase_signals: block in frontmatter'); -}); - -test('v5.1 — HANDOVER-CONTRACTS.md schema row includes phase_signals + phase_signals_partial', () => { - const t = read('docs/HANDOVER-CONTRACTS.md'); - assert.ok(t.includes('| `phase_signals` |'), - 'HANDOVER-CONTRACTS must add a phase_signals row to the Handover 1 schema table'); - assert.ok(t.includes('| `phase_signals_partial` |'), - 'HANDOVER-CONTRACTS must add a phase_signals_partial row to the Handover 1 schema table'); -}); - -test('v5.1 — voyage CLAUDE.md mentions phase_signals', () => { - const t = read('CLAUDE.md'); - assert.ok(t.includes('phase_signals'), - 'voyage CLAUDE.md must document phase_signals (v5.1)'); -}); - -test('v5.1 — voyage README.md mentions phase_signals', () => { - const t = read('README.md'); - assert.ok(t.includes('phase_signals'), - 'voyage README.md must mention phase_signals (v5.1 "What\'s new" bullet)'); -}); - -// --- v5.1.1 — High-effort behavior sub-section per command (Step 10) --- - -test('v5.1.1 — commands/trekplan.md contains ### High-effort behavior (v5.1.1) sub-section', () => { - const t = read('commands/trekplan.md'); - assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m, - 'trekplan.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (Decision B + gemini-bridge)'); -}); - -test('v5.1.1 — commands/trekresearch.md contains ### High-effort behavior (v5.1.1) sub-section', () => { - const t = read('commands/trekresearch.md'); - assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m, - 'trekresearch.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (contrarian-researcher + gemini-bridge always-on)'); -}); - -test('v5.1.1 — commands/trekreview.md contains ### High-effort behavior (v5.1.1) sub-section', () => { - const t = read('commands/trekreview.md'); - assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m, - 'trekreview.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (skip Pass 3 + coordinator normalization)'); -}); - -test('v5.1.1 — commands/trekexecute.md contains ### High-effort behavior (v5.1.1) sub-section', () => { - const t = read('commands/trekexecute.md'); - assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m, - 'trekexecute.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (gates_mode = closed)'); -}); diff --git a/plugins/voyage/tests/lib/phase-signal-resolver.test.mjs b/plugins/voyage/tests/lib/phase-signal-resolver.test.mjs deleted file mode 100644 index 5461740..0000000 --- a/plugins/voyage/tests/lib/phase-signal-resolver.test.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { execFileSync } from 'node:child_process'; -import { writeFileSync, unlinkSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { resolvePhaseSignal, resolvePhaseSignalFromFile } from '../../lib/profiles/phase-signal-resolver.mjs'; - -const FULL_SIGNALS_FM = { - phase_signals: [ - { phase: 'research', effort: 'low', model: 'sonnet' }, - { phase: 'plan', effort: 'standard' }, - { phase: 'execute', effort: 'high', model: 'opus' }, - { phase: 'review', effort: 'standard', model: 'sonnet' }, - ], -}; - -test('resolvePhaseSignal — returns {effort, model} for all 4 phases on full-signals brief', () => { - for (const phase of ['research', 'plan', 'execute', 'review']) { - const r = resolvePhaseSignal(FULL_SIGNALS_FM, phase); - assert.ok(r && typeof r === 'object', `phase=${phase} should resolve non-null`); - assert.ok(typeof r.effort === 'string', `phase=${phase} should have effort`); - } -}); - -test('resolvePhaseSignal — returns null when brief has no phase_signals', () => { - const r = resolvePhaseSignal({ task: 'x' }, 'plan'); - assert.equal(r, null); -}); - -test('resolvePhaseSignal — returns partial {effort} with model undefined when signal omits model', () => { - const r = resolvePhaseSignal(FULL_SIGNALS_FM, 'plan'); - assert.equal(r.effort, 'standard'); - assert.equal(r.model, undefined); - assert.ok(!('model' in r), 'model key should be absent when not in signal'); -}); - -test('resolvePhaseSignal — returns null when phase is not in PHASE_SIGNAL_PHASES', () => { - assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'brief'), null); - assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'continue'), null); - assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'nonsense'), null); -}); - -test('resolvePhaseSignal — defensive: null/non-object input returns null', () => { - assert.equal(resolvePhaseSignal(null, 'plan'), null); - assert.equal(resolvePhaseSignal(undefined, 'plan'), null); - assert.equal(resolvePhaseSignal('string', 'plan'), null); - assert.equal(resolvePhaseSignal({ phase_signals: 'not-array' }, 'plan'), null); -}); - -test('resolvePhaseSignalFromFile + CLI shim — writes JSON to stdout, exit 0', () => { - const fixture = join(tmpdir(), `phase-signal-test-${process.pid}.md`); - writeFileSync(fixture, `--- -type: trekbrief -brief_version: "2.1" -phase_signals: - - phase: plan - effort: high - model: opus ---- -# x -`); - try { - // Programmatic invocation - const r = resolvePhaseSignalFromFile(fixture, 'plan'); - assert.deepEqual(r, { effort: 'high', model: 'opus' }); - // CLI shim - const helperPath = new URL('../../lib/profiles/phase-signal-resolver.mjs', import.meta.url).pathname; - const out = execFileSync('node', [helperPath, '--brief', fixture, '--phase', 'plan', '--json'], { - encoding: 'utf-8', - }); - const parsed = JSON.parse(out.trim()); - assert.deepEqual(parsed, { effort: 'high', model: 'opus' }); - } finally { - try { unlinkSync(fixture); } catch { /* swallow */ } - } -}); diff --git a/plugins/voyage/tests/lib/profile-resolver.test.mjs b/plugins/voyage/tests/lib/profile-resolver.test.mjs deleted file mode 100644 index 4eef940..0000000 --- a/plugins/voyage/tests/lib/profile-resolver.test.mjs +++ /dev/null @@ -1,62 +0,0 @@ -// tests/lib/profile-resolver.test.mjs -// v5.1.1 SC5 — non-interference cases for resolvePhaseModel(). -// Verifies the new highest-priority lookup step (brief.phase_signals[phase].model) -// wins over --profile flag and VOYAGE_PROFILE env; falls through cleanly when -// no brief signal is present. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { resolvePhaseModel } from '../../lib/profiles/resolver.mjs'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = join(__dirname, '..', '..'); -const FIXTURE = (name) => join(REPO_ROOT, 'tests', 'fixtures', name); - -test('resolvePhaseModel — Case 1: brief signal wins over VOYAGE_PROFILE env', () => { - // brief-effort-low.md pins all 4 phases to model: sonnet. - // env says premium (would normally select opus). Brief must win. - const r = resolvePhaseModel('research', FIXTURE('brief-effort-low.md'), [], { VOYAGE_PROFILE: 'premium' }); - assert.equal(r.model, 'sonnet', `brief signal should beat env; got ${JSON.stringify(r)}`); - assert.equal(r.source, 'brief-signal'); -}); - -test('resolvePhaseModel — Case 2: brief signal wins over --profile flag', () => { - // brief-effort-high.md pins all 4 phases to model: opus. - // flag says economy (would normally select sonnet). Brief must win. - const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'economy'], {}); - assert.equal(r.model, 'opus', `brief signal should beat flag; got ${JSON.stringify(r)}`); - assert.equal(r.source, 'brief-signal'); -}); - -test('resolvePhaseModel — Case 3: no phase_signals → fallthrough to --profile flag', () => { - // brief-without-phase-signals fixture lacks phase_signals entirely. - // --profile balanced is set. Should return balanced.phase_models.plan (= opus per yaml). - const r = resolvePhaseModel('plan', FIXTURE('brief-without-phase-signals.md'), ['--profile', 'balanced'], {}); - assert.equal(r.model, 'opus', `balanced.plan should be opus; got ${JSON.stringify(r)}`); - assert.equal(r.source, 'flag'); -}); - -test('resolvePhaseModel — Case 4: phase not in PHASE_SIGNAL_PHASES falls through gracefully', () => { - // brief-effort-high.md has signals for the 4 supported phases. - // Asking for 'continue' (not in PHASE_SIGNAL_PHASES) must fall through. - // --profile premium is set, so continue resolves to premium.phase_models.continue (= opus). - const r = resolvePhaseModel('continue', FIXTURE('brief-effort-high.md'), ['--profile', 'premium'], {}); - assert.equal(r.model, 'opus', `premium.continue should be opus; got ${JSON.stringify(r)}`); - assert.ok(r.source !== 'brief-signal', 'continue must not resolve via brief-signal'); -}); - -test('resolvePhaseModel — Case 5 (defensive): missing brief file falls through cleanly', () => { - // Non-existent path. Must not throw; must fall through to flag/env/default. - const r = resolvePhaseModel('plan', '/nonexistent/brief.md', ['--profile', 'economy'], {}); - assert.equal(r.model, 'sonnet', 'economy.plan should be sonnet on fallthrough'); - assert.equal(r.source, 'flag'); -}); - -test('resolvePhaseModel — Case 6 (defensive): null briefPath falls through to default', () => { - // null briefPath, no flag, no env → default = premium. - const r = resolvePhaseModel('plan', null, [], {}); - assert.equal(r.model, 'opus', 'premium.plan default = opus'); - assert.equal(r.source, 'default'); -}); diff --git a/plugins/voyage/tests/scripts/annotate.test.mjs b/plugins/voyage/tests/scripts/annotate.test.mjs deleted file mode 100644 index 3044447..0000000 --- a/plugins/voyage/tests/scripts/annotate.test.mjs +++ /dev/null @@ -1,208 +0,0 @@ -// tests/scripts/annotate.test.mjs -// Covers scripts/annotate.mjs — the v5.0.3 operator-annotation HTML -// generator. UX modelled on claude-code-100x/build-site.js (pencil -// toggle, intent buttons, form popover, selection-anchoring, localStorage -// persistence, structured markdown export). -// -// What we pin: -// • Output is a complete, self-contained HTML document. -// • No external or "\n---\n\n# Foo\n'; - const html = buildHtml('/abs/path/brief.md', md); - const titleMatch = html.match(/([\s\S]*?)<\/title>/); - assert.ok(titleMatch, 'must have a title'); - assert.ok(!titleMatch[1].includes('<script>'), 'title must not carry a raw <script> tag'); - assert.match(titleMatch[1], /<script>/, 'title must be HTML-escaped'); -}); - -test('hostile inline content cannot inject as live HTML attributes', () => { - const md = '# Heading\n\nA paragraph with <img src=x onerror="alert(1)"> embedded.\n'; - const html = buildHtml('/abs/path/brief.md', md); - // The article body must not carry a live onerror="..." attribute (the renderer - // HTML-escapes everything in the body, so `<` → `<`). - const articleMatch = html.match(/<article[^>]*>([\s\S]*?)<\/article>/); - assert.ok(articleMatch, 'must have article body'); - assert.ok(!/onerror\s*=\s*"alert/i.test(articleMatch[1]), - 'article body must not carry a live onerror attribute'); - assert.ok(articleMatch[1].includes('<img'), - 'hostile <img> must be escaped to <img'); -}); - -test('render() is deterministic — two runs byte-identical', () => { - const dir = mkdtempSync(join(tmpdir(), 'claude-annotate-')); - try { - const md = join(dir, 'plan.md'); - writeFileSync(md, SAMPLE); - const a = render(md, join(dir, 'a.html')); - const b = render(md, join(dir, 'b.html')); - assert.ok(existsSync(a) && existsSync(b)); - assert.equal(readFileSync(a, 'utf-8'), readFileSync(b, 'utf-8')); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('render() defaults output to <input-basename>.html next to input', () => { - const dir = mkdtempSync(join(tmpdir(), 'claude-annotate-')); - try { - const md = join(dir, 'review.md'); - writeFileSync(md, '# Review\n\nok\n'); - const out = render(md); - assert.equal(out, join(dir, 'review.html')); - assert.ok(existsSync(out)); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('parseArgs handles --out, positional input, and --help', () => { - assert.deepEqual(parseArgs(['x.md']), { input: 'x.md', out: null, help: false }); - assert.deepEqual(parseArgs(['x.md', '--out', 'y.html']), { input: 'x.md', out: 'y.html', help: false }); - assert.equal(parseArgs(['--help']).help, true); -}); - -test('buildHtml wires the v5.0.3 operator-driven annotation affordances', () => { - // Pin every UX-critical affordance modelled on claude-code-100x/build-site.js: - // - Pencil-toggle button (annotation mode on/off) - // - Form popover with three intent buttons (Fiks/Endre/Spørsmål) - // - Annotations sidebar (Your annotations + Clear all + Copy Prompt) - // - Selection capture (window.getSelection()) - // - Section context auto-detection (findSection) - // - localStorage persistence (voyage-annotate:v2:...) - // - Annotatable elements (data-anchor-id on h1-h6, p, li, td, blockquote, pre) - const html = buildHtml('/abs/path/brief.md', SAMPLE); - // Toggle - assert.ok(html.includes('ann-toggle'), 'must have the pencil-toggle button'); - assert.ok(html.includes('Annotation mode: ON'), 'must label the toggle state'); - // Form + intents (the three CSS classes for selected state) - assert.ok(html.includes('data-intent="fiks"'), 'must have Fiks intent button'); - assert.ok(html.includes('data-intent="endre"'), 'must have Endre intent button'); - assert.ok(html.includes('data-intent="spørsmål"'), 'must have Spørsmål intent button'); - // Form popover - assert.ok(html.includes('ann-form'), 'must have the form popover'); - assert.ok(html.includes('ann-form-comment'), 'must have a comment textarea'); - assert.ok(html.includes('ann-form-save'), 'must have a Save button'); - // Sidebar - assert.ok(html.includes('ann-panel'), 'must have the annotations sidebar'); - assert.ok(html.includes('Your annotations'), 'sidebar must title the list'); - assert.ok(html.includes('Clear all'), 'sidebar must offer Clear all'); - assert.ok(html.includes('Copy Prompt'), 'sidebar must offer Copy Prompt'); - // Selection + section - assert.ok(html.includes('window.getSelection'), 'must capture selection'); - assert.ok(html.includes('findSection'), 'must auto-detect section context'); - // Persistence - assert.ok(html.includes("'voyage-annotate:v2:'"), 'must use the v2 localStorage key prefix'); - // Anchor coverage - const anchors = (html.match(/data-anchor-id="anch-/g) || []).length; - assert.ok(anchors >= 5, 'must emit data-anchor-id on enough elements (got ' + anchors + ')'); -}); - -test('renderMarkdown produces headings, lists, code, table, blockquote with anchors', () => { - const html = renderMarkdown(`# H1 -## H2 -- a -- b - -1. one -2. two - -| Col | Val | -|-----|-----| -| x | 1 | - -\`\`\` -plain code -\`\`\` - -> quote -`); - assert.match(html, /<h1 data-anchor-id="anch-0">H1<\/h1>/); - assert.match(html, /<h2 data-anchor-id="anch-1">H2<\/h2>/); - assert.match(html, /<ul><li data-anchor-id=/); - assert.match(html, /<ol><li data-anchor-id=/); - assert.match(html, /<table>[\s\S]*<th data-anchor-id=/); - assert.match(html, /<pre data-anchor-id=/); - assert.match(html, /<blockquote data-anchor-id=/); -}); diff --git a/plugins/voyage/tests/validators/brief-validator.test.mjs b/plugins/voyage/tests/validators/brief-validator.test.mjs index 69e250f..6e501d2 100644 --- a/plugins/voyage/tests/validators/brief-validator.test.mjs +++ b/plugins/voyage/tests/validators/brief-validator.test.mjs @@ -152,101 +152,3 @@ test('validateBrief — wrong-type error message includes accepted set', () => { assert.ok(/trekbrief/.test(wrongType.message)); assert.ok(/trekreview/.test(wrongType.message)); }); - -// --- v5.1 — phase_signals additive field + sequencing gate --- - -const SIGNALS_BLOCK = `phase_signals: - - phase: research - effort: standard - - phase: plan - effort: high - model: opus - - phase: execute - effort: low - model: sonnet - - phase: review - effort: standard -`; - -test('validateBrief — v5.1 well-formed phase_signals accepted', () => { - const t = GOOD_BRIEF - .replace('brief_version: "2.0"', 'brief_version: "2.1"') - .replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, true, JSON.stringify(r.errors)); -}); - -test('validateBrief — pre-v5.1 brief without phase_signals accepted (backward-compat)', () => { - const r = validateBriefContent(GOOD_BRIEF, { strict: true }); - assert.equal(r.valid, true, JSON.stringify(r.errors)); - assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); -}); - -test('validateBrief — v5.1+ brief missing phase_signals + partial emits BRIEF_V51_MISSING_SIGNALS', () => { - const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"'); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); -}); - -test('validateBrief — v5.1+ brief with phase_signals_partial: true accepted', () => { - const t = GOOD_BRIEF - .replace('brief_version: "2.0"', 'brief_version: "2.1"') - .replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n'); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, true, JSON.stringify(r.errors)); -}); - -test('validateBrief — phase_signals + phase_signals_partial both set rejected (mutually exclusive)', () => { - const t = GOOD_BRIEF - .replace('brief_version: "2.0"', 'brief_version: "2.1"') - .replace('source: interview\n', `source: interview\nphase_signals_partial: true\n${SIGNALS_BLOCK}`); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE')); -}); - -test('validateBrief — phase_signals with unknown phase rejected', () => { - const BAD_SIGNALS = `phase_signals: - - phase: nonsense - effort: standard -`; - const t = GOOD_BRIEF - .replace('brief_version: "2.0"', 'brief_version: "2.1"') - .replace('source: interview\n', `source: interview\n${BAD_SIGNALS}`); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_PHASE_SIGNAL_PHASE')); -}); - -// --- v5.1.1 regression: YAML-number bypass closed --- -// Findings 3c834097 + df1435a2: v5.1.0 shipped with an unquoted `brief_version: 2.1` -// template. parseScalar coerces unquoted "2.1" to Number 2.1, and the original gate -// guarded `typeof === 'string'`, silently bypassing the sequencing check. v5.1.1 -// coerces via String() so both shapes trigger the gate. - -test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 without signals triggers gate', () => { - const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: 2.1'); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, false); - assert.ok( - r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), - `gate must fire for unquoted brief_version: 2.1 (YAML Number); errors=${JSON.stringify(r.errors)}`, - ); -}); - -test('validateBrief — v5.1.1: QUOTED brief_version "2.1" without signals triggers gate (regression guard)', () => { - const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"'); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); -}); - -test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 WITH phase_signals is valid (positive case)', () => { - const t = GOOD_BRIEF - .replace('brief_version: "2.0"', 'brief_version: 2.1') - .replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`); - const r = validateBriefContent(t, { strict: true }); - assert.equal(r.valid, true, JSON.stringify(r.errors)); - assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); -}); diff --git a/shared/playground-design-system/CHANGELOG.md b/shared/playground-design-system/CHANGELOG.md index 3d489a7..1594aa0 100644 --- a/shared/playground-design-system/CHANGELOG.md +++ b/shared/playground-design-system/CHANGELOG.md @@ -1,53 +1,5 @@ # playground-design-system — CHANGELOG -## 0.6.0 — 2026-05-15 - -### Added — Project-view archetype (Tier 4) - -Generic "project as artifact-collection" archetype for plugins where a project owns 0-N read-only report artifacts grouped by category. Default view is an aggregated dashboard; clicking a sidebar item swaps the main panel to the per-artifact render. Edit-mode is paste-import only (no inline editor). - -- **New file `components-tier4-project-view.css`** — 11 sections covering: - - `.project-view` + `.project-view__layout` (grid: nav 280px + main 1fr, responsive collapse at 1280 / 960px) - - `.project-view__header` (CSS Grid with eyebrow/title/lede/verdict/key-stats/actions areas) - - `.verdict-pill` (small pill variant — companion to existing `.verdict-pill-lg` in tier2) - - `.project-view__nav` + `.project-view__nav-search` (sticky sidebar with search) - - `.artifact-list` + `__group` / `__group-label` / `__group-count` / `__group-items` / `__item` / `__item-marker` / `__item-body` / `__item-name` / `__item-meta` (grouped, severity-coded sidebar) - - `.artifact-status[data-severity]` (mini-pill: positive | medium | critical) - - `.project-view__main` (main column container) - - `.project-overview` + `__intro` / `__verdict-grid` / `__verdict-tile[data-severity]` / `__section` / `__top-risks` / `__next-actions` / `__missing-reports` (aggregated dashboard) - - `.project-view__artifact` + `__artifact-header` / `__artifact-title` / `__artifact-meta` / `__artifact-actions` / `__artifact-body` (single-rapport viewer wrapper) - - `.empty-artifact-prompt` + `__icon` / `__title` / `__text` / `__actions` (empty-state) - - `.import-modal` + `__backdrop` / `__panel` / `__head` / `__title` / `__close` / `__form` / `__detect` / `__preview` / `__preview-label` / `__footer` (overlay modal for paste-import) - -- **6 new tokens in `tokens.css`:** - - `--project-view-nav-width: 280px` — sidebar width at full layout - - `--project-view-collapse-bp: 960px` — doc-only token referenced by responsive breakpoints - - `--artifact-list-item-pad-y: var(--space-2)` — sidebar row vertical padding - - `--artifact-list-item-pad-x: var(--space-3)` — sidebar row horizontal padding - - `--artifact-marker-size: 14px` — sidebar status marker diameter - - `--artifact-marker-border: 1.5px` — sidebar status marker border thickness - -### Påvirkning - -Endringen er **additiv**: ny komponent-fil + 6 nye tokens, ingen eksisterende selectors eller verdier endres. Plugin-konsumenter (`ms-ai-architect`, `llm-security`, `okr`, `config-audit`, `voyage`) får silent drift mot ny source-commit, men kan re-sync på eget tempo. Bare `ms-ai-architect` og `llm-security` re-syncer i samme commit som denne DS-bumpen (forberedelse til koordinert v1.15.0 / v7.7.0-release etter ~8 sesjoner med JS-implementasjon). - -Førsteadoptere: `ms-ai-architect` v1.15.0 (17 artefakter, 5 kategorier) + `llm-security` v7.7.0 (≥18 artefakter, 6 kategorier). State-driven visibility håndteres i plugin-JS, ikke i denne CSS-en — kun aktiv state rendres per pass. - -### Plugins som må laste den nye filen - -Etter `<link>` til `components-tier3-supplement.css`, legg til: - -```html -<link rel="stylesheet" href="vendor/playground-design-system/components-tier4-project-view.css"> -``` - -### For å adoptere v0.6.0 - -```bash -node scripts/sync-design-system.mjs <plugin-name> -# --force hvis drift detected -``` - ## 0.5.0 — 2026-05-10 ### Added diff --git a/shared/playground-design-system/components-tier4-project-view.css b/shared/playground-design-system/components-tier4-project-view.css deleted file mode 100644 index 3db37b8..0000000 --- a/shared/playground-design-system/components-tier4-project-view.css +++ /dev/null @@ -1,665 +0,0 @@ -/* ============================================================================= - Playground Design System — components-tier4-project-view.css - v0.6.0 — Tier 4 project-view archetype - ============================================================================ - - Generic "project as artifact-collection" archetype. Default-view is an - aggregated overview dashboard; clicking a sidebar item swaps main to a - per-artifact render. Tracks 0-N read-only artifacts; edit-mode is paste- - import only (markdown from terminal → parser → store). - - First adopters: ms-ai-architect v1.15.0 (17 artifacts, 5 categories) + - llm-security v7.7.0 (≥18 artifacts, 6 categories). Each plugin injects a - PROJECT_VIEW_CONFIG object that maps commands → renderers, categories, - verdict-aggregators, missing-report heuristics. - - The CSS in this file is plugin-agnostic. Plugin-specific shape (category - names, artifact ordering, custom severity-mappings) lives in JS config. - - State-driven visibility is NOT handled here — production playgrounds emit - only the active state (overview | artifact | empty | import) per render - pass. The mockup uses body[data-state="..."] for prototyping; production - renders one branch at a time. - ============================================================================= */ - - -/* === 1. Project-view top-level layout ===================================== */ - -.project-view { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -.project-view__layout { - display: grid; - grid-template-columns: var(--project-view-nav-width) 1fr; - gap: var(--space-6); - align-items: start; -} - -@media (max-width: 1279px) { - .project-view__layout { grid-template-columns: 240px 1fr; } -} - -@media (max-width: 959px) { - .project-view__layout { grid-template-columns: 1fr; } -} - - -/* === 2. Project-view header =============================================== */ - -.project-view__header { - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-md); - padding: var(--space-5) var(--space-6); - display: grid; - grid-template-columns: 1fr auto; - grid-template-areas: - "title verdict" - "title keystats" - "actions actions"; - gap: var(--space-4) var(--space-6); - align-items: start; -} - -.project-view__title-block { grid-area: title; } -.project-view__verdict { grid-area: verdict; justify-self: end; } -.project-view__key-stats { grid-area: keystats; justify-self: end; } -.project-view__actions { grid-area: actions; display: flex; gap: var(--space-2); justify-content: flex-end; } - -.project-view__eyebrow { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-text-tertiary); - font-weight: var(--font-weight-semibold); - margin: 0 0 var(--space-2) 0; -} - -.project-view__title { - font-size: var(--font-size-2xl); - font-weight: var(--font-weight-bold); - color: var(--color-text-primary); - margin: 0 0 var(--space-2) 0; -} - -.project-view__lede { - color: var(--color-text-secondary); - margin: 0; - max-width: 60ch; -} - -.project-view__key-stats { - display: flex; - gap: var(--space-5); -} - -.project-view__key-stat-label { - font-size: 10px; - text-transform: uppercase; - color: var(--color-text-tertiary); - letter-spacing: 0.06em; -} - -.project-view__key-stat-value { - font-size: var(--font-size-lg); - font-weight: var(--font-weight-semibold); - font-variant-numeric: tabular-nums; -} - - -/* === 3. Verdict-pill (small) ============================================== - Companion to .verdict-pill-lg (Tier 2). Inline-flex pill used in project - header + sidebar status badges. The larger -lg variant lives in - components-tier2.css; both share the same severity-band semantics. */ - -.verdict-pill { - display: inline-flex; - align-items: center; - gap: var(--space-1); - padding: 4px 12px; - border-radius: 999px; - font-weight: var(--font-weight-semibold); - font-size: var(--font-size-sm); -} - -.verdict-pill--positive { background: var(--color-state-success); color: #fff; } -.verdict-pill--medium { background: var(--color-severity-medium); color: var(--color-severity-medium-on); } -.verdict-pill--critical { background: var(--color-severity-critical); color: #fff; } -.verdict-pill--in-progress { - background: var(--color-bg-soft); - color: var(--color-text-secondary); - border: 1px dashed var(--color-border-moderate); -} - - -/* === 4. Sidebar nav ======================================================= */ - -.project-view__nav { - position: sticky; - top: var(--space-6); - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-md); - padding: var(--space-4); - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.project-view__nav-search input { - width: 100%; - box-sizing: border-box; - padding: 6px 10px; - font-size: var(--font-size-sm); - background: var(--color-bg); - color: var(--color-text-primary); - border: 1px solid var(--color-border-moderate); - border-radius: var(--radius-sm); -} - - -/* === 5. Artifact-list ===================================================== */ - -.artifact-list { - display: flex; - flex-direction: column; - gap: var(--space-4); - margin: 0; - padding: 0; - list-style: none; -} - -.artifact-list__group { - display: flex; - flex-direction: column; - gap: var(--space-1); -} - -.artifact-list__group-label { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-text-tertiary); - font-weight: var(--font-weight-semibold); - padding: 0 var(--space-2); -} - -.artifact-list__group-count { - background: var(--color-bg-soft); - color: var(--color-text-tertiary); - font-family: var(--font-family-mono); - font-size: 10px; - padding: 1px 6px; - border-radius: 999px; -} - -.artifact-list__group-items { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.artifact-list__item { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: var(--space-2); - padding: var(--artifact-list-item-pad-y) var(--artifact-list-item-pad-x); - border-radius: var(--radius-sm); - cursor: pointer; - background: transparent; - border: 1px solid transparent; - transition: background 120ms ease, border-color 120ms ease; -} - -.artifact-list__item:hover { background: var(--color-bg-soft); } - -.artifact-list__item[data-state="active"] { - background: var(--color-bg-soft); - border-color: var(--color-primary-500); - box-shadow: inset 3px 0 0 var(--color-primary-500); - padding-left: calc(var(--artifact-list-item-pad-x) - 3px); -} - -.artifact-list__item-marker { - width: var(--artifact-marker-size); - height: var(--artifact-marker-size); - border-radius: 50%; - border: var(--artifact-marker-border) solid var(--color-border-moderate); - background: transparent; - flex-shrink: 0; -} - -.artifact-list__item[data-state="filled"][data-severity="positive"] .artifact-list__item-marker { - background: var(--color-state-success); - border-color: var(--color-state-success); -} -.artifact-list__item[data-state="filled"][data-severity="medium"] .artifact-list__item-marker { - background: var(--color-severity-medium); - border-color: var(--color-severity-medium); -} -.artifact-list__item[data-state="filled"][data-severity="critical"] .artifact-list__item-marker { - background: var(--color-severity-critical); - border-color: var(--color-severity-critical); -} - -.artifact-list__item-body { min-width: 0; } - -.artifact-list__item-name { - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); - color: var(--color-text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.artifact-list__item[data-state="empty"] .artifact-list__item-name { - color: var(--color-text-tertiary); - font-weight: var(--font-weight-regular); -} - -.artifact-list__item-meta { - font-size: 11px; - color: var(--color-text-tertiary); -} - - -/* === 6. Artifact-status (mini pill in sidebar) =========================== */ - -.artifact-status { - font-family: var(--font-family-mono); - font-size: 10px; - font-weight: var(--font-weight-semibold); - padding: 1px 5px; - border-radius: var(--radius-sm); - letter-spacing: 0.04em; -} - -.artifact-status[data-severity="positive"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); } -.artifact-status[data-severity="medium"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); } -.artifact-status[data-severity="critical"] { background: var(--color-severity-critical-soft); color: var(--color-severity-critical-on); } - - -/* === 7. Project-view main panel ========================================== */ - -.project-view__main { - min-width: 0; - display: flex; - flex-direction: column; - gap: var(--space-5); -} - - -/* === 8. Project-overview (default dashboard) ============================= */ - -.project-overview { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -.project-overview__intro { - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-md); - padding: var(--space-5); -} - -.project-overview__intro h2 { - font-size: var(--font-size-lg); - margin: 0 0 var(--space-2) 0; -} - -.project-overview__intro p { - color: var(--color-text-secondary); - margin: 0; -} - -.project-overview__verdict-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-3); -} - -.project-overview__verdict-tile { - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-left: 4px solid var(--color-border-moderate); - border-radius: var(--radius-md); - padding: var(--space-4); - display: flex; - flex-direction: column; - gap: var(--space-1); -} - -.project-overview__verdict-tile[data-severity="positive"] { border-left-color: var(--color-state-success); } -.project-overview__verdict-tile[data-severity="medium"] { border-left-color: var(--color-severity-medium); } -.project-overview__verdict-tile[data-severity="critical"] { border-left-color: var(--color-severity-critical); } -.project-overview__verdict-tile[data-severity="empty"] { border-left-style: dashed; } - -.project-overview__verdict-tile-label { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-text-tertiary); - font-weight: var(--font-weight-semibold); -} - -.project-overview__verdict-tile-value { - font-size: var(--font-size-lg); - font-weight: var(--font-weight-semibold); -} - -.project-overview__verdict-tile-meta { - font-size: var(--font-size-xs); - color: var(--color-text-secondary); -} - -.project-overview__section h3 { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-text-tertiary); - font-weight: var(--font-weight-semibold); - margin: 0 0 var(--space-3) 0; -} - -.project-overview__top-risks, -.project-overview__next-actions { - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-md); - padding: var(--space-5); -} - -.project-overview__top-risks ol, -.project-overview__next-actions ol { - list-style: none; - counter-reset: rank; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.project-overview__top-risks li, -.project-overview__next-actions li { - counter-increment: rank; - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: var(--space-3); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - background: var(--color-bg-soft); -} - -.project-overview__top-risks li::before, -.project-overview__next-actions li::before { - content: counter(rank); - font-family: var(--font-family-mono); - font-weight: var(--font-weight-bold); - color: var(--color-text-tertiary); - font-size: var(--font-size-sm); - min-width: 20px; -} - -.project-overview__missing-reports { - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-md); - padding: var(--space-5); -} - -.project-overview__missing-reports ul { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.project-overview__missing-reports li { - display: flex; - justify-content: space-between; - align-items: center; - gap: var(--space-3); - padding: var(--space-2) var(--space-3); - background: var(--color-bg-soft); - border-radius: var(--radius-sm); - border-left: 3px dashed var(--color-border-moderate); -} - - -/* === 9. Artifact-view (one report rendered) ============================== */ - -.project-view__artifact { - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-md); - padding: var(--space-6); - display: flex; - flex-direction: column; - gap: var(--space-5); -} - -.project-view__artifact-header { - display: flex; - justify-content: space-between; - align-items: start; - gap: var(--space-4); - padding-bottom: var(--space-4); - border-bottom: 1px solid var(--color-border-subtle); -} - -.project-view__artifact-title { - font-size: var(--font-size-xl); - margin: 0 0 var(--space-1) 0; -} - -.project-view__artifact-meta { - font-size: var(--font-size-sm); - color: var(--color-text-tertiary); - margin: 0; -} - -.project-view__artifact-actions { - display: flex; - gap: var(--space-2); - flex-shrink: 0; -} - -.project-view__artifact-body { - display: flex; - flex-direction: column; - gap: var(--space-5); -} - - -/* === 10. Empty-artifact-prompt (no report imported yet) ================== */ - -.empty-artifact-prompt { - background: var(--color-surface); - border: 2px dashed var(--color-border-moderate); - border-radius: var(--radius-md); - padding: var(--space-8); - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-3); - text-align: center; -} - -.empty-artifact-prompt__icon { - font-size: 48px; - opacity: 0.5; -} - -.empty-artifact-prompt__title { - font-size: var(--font-size-lg); - margin: 0; -} - -.empty-artifact-prompt__text { - color: var(--color-text-secondary); - margin: 0; - max-width: 50ch; -} - -.empty-artifact-prompt__actions { - display: flex; - gap: var(--space-2); - margin-top: var(--space-2); -} - - -/* === 11. Import-modal (overlay) ========================================== */ - -.import-modal { - position: fixed; - inset: 0; - z-index: 200; - display: none; -} - -.import-modal[data-open="true"] { - display: flex; - align-items: center; - justify-content: center; -} - -.import-modal__backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.55); -} - -.import-modal__panel { - position: relative; - width: min(720px, 92vw); - max-height: 90vh; - overflow: auto; - background: var(--color-surface); - border: 1px solid var(--color-border-strong); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - display: flex; - flex-direction: column; -} - -.import-modal__head { - display: flex; - justify-content: space-between; - align-items: center; - gap: var(--space-3); - padding: var(--space-4) var(--space-5); - border-bottom: 1px solid var(--color-border-subtle); -} - -.import-modal__title { - margin: 0; - font-size: var(--font-size-lg); - font-weight: var(--font-weight-semibold); -} - -.import-modal__close { - background: transparent; - border: none; - cursor: pointer; - padding: 4px 10px; - color: var(--color-text-tertiary); - font-size: 20px; - line-height: 1; - border-radius: var(--radius-sm); -} - -.import-modal__close:hover { - background: var(--color-bg-soft); - color: var(--color-text-primary); -} - -.import-modal__form { - padding: var(--space-5); - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.import-modal__form .field { - display: flex; - flex-direction: column; - gap: var(--space-1); -} - -.import-modal__form label { - font-size: var(--font-size-sm); - font-weight: var(--font-weight-semibold); - color: var(--color-text-secondary); -} - -.import-modal__form select, -.import-modal__form textarea { - width: 100%; - box-sizing: border-box; - padding: var(--space-2) var(--space-3); - background: var(--color-bg); - color: var(--color-text-primary); - border: 1px solid var(--color-border-moderate); - border-radius: var(--radius-sm); - font-family: var(--font-family-mono); - font-size: var(--font-size-sm); -} - -.import-modal__form textarea { - resize: vertical; - min-height: 180px; -} - -.import-modal__detect { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - background: var(--color-severity-low-soft); - color: var(--color-severity-low-on); - font-size: var(--font-size-sm); -} - -.import-modal__preview { - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-sm); - padding: var(--space-3); - background: var(--color-bg); - max-height: 200px; - overflow: auto; -} - -.import-modal__preview-label { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-text-tertiary); - margin-bottom: var(--space-2); -} - -.import-modal__footer { - display: flex; - justify-content: flex-end; - gap: var(--space-2); - padding: var(--space-3) var(--space-5); - border-top: 1px solid var(--color-border-subtle); - background: var(--color-bg-soft); -} diff --git a/shared/playground-design-system/tokens.css b/shared/playground-design-system/tokens.css index 1686a5c..95ef620 100644 --- a/shared/playground-design-system/tokens.css +++ b/shared/playground-design-system/tokens.css @@ -142,14 +142,6 @@ --container-default: 1080px; --container-wide: 1280px; --sidebar-width: 280px; - - /* ---------- Project-view (Tier 4 — v0.6.0) --------------------------- */ - --project-view-nav-width: 280px; - --project-view-collapse-bp: 960px; /* doc-only — referenced by media queries */ - --artifact-list-item-pad-y: var(--space-2); - --artifact-list-item-pad-x: var(--space-3); - --artifact-marker-size: 14px; - --artifact-marker-border: 1.5px; } :root { color-scheme: light; }