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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/.gitattributes text eol=lf

# Preserve exact bytes used by runtime package hash checks.
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/ui/vendor/markdown-reader/** -text linguist-generated=true
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/server/artifact-read.mjs -text
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/server/artifact-review.mjs -text
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/server/artifact-discovery.mjs -text
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/server/artifact-clarifications.mjs -text linguist-generated=true
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/server/artifact-clarifications.NOTICES.txt -text linguist-generated=true
/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/ui/artifact-review.js -text
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/vendor/markdown-reader/** -text linguist-generated=true
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/vendor/artifact-read.mjs -text linguist-generated=true
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/vendor/artifact-review.mjs -text linguist-generated=true
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/vendor/artifact-discovery.mjs -text linguist-generated=true
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/vendor/artifact-clarifications.mjs -text linguist-generated=true
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/vendor/artifact-clarifications.NOTICES.txt -text linguist-generated=true
/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/ui/artifact-review.js -text linguist-generated=true
111 changes: 111 additions & 0 deletions .github/workflows/canvas-reader-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Canvas Reader Validation

on:
workflow_dispatch:
pull_request:
paths:
- "plugins/spec-kit-copilot-wizard/**"
- "plugins/spec-kit-copilot-sdd/**"
- "scripts/canvas-reader/**"
- "docs/markdown-artifact-review.md"
- ".github/workflows/canvas-reader-validation.yml"

permissions:
contents: read

jobs:
validate:
name: Automated checks (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 25
defaults:
run:
shell: pwsh
working-directory: plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/ui/markdown-reader
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 24.19.0
cache: npm
cache-dependency-path: plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/ui/markdown-reader/package-lock.json

- name: Install locked build dependencies without lifecycle scripts
run: |
npm ci --ignore-scripts --no-fund
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Install existing Wizard runtime from its lockfile
run: |
npm ci --prefix ../.. --ignore-scripts --no-fund
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Audit dependencies
run: |
npm audit --audit-level=low --ignore-scripts
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Typecheck configuration
run: |
npm run typecheck
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Lint implemented surfaces
run: |
npm run lint
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Build without repairing committed payload copies
run: |
npm run build:reader
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
npm run build:parser
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
npm run verify:package
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Reader rendering and security tests
run: |
npm test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Wizard and SDD regression and security suites
env:
CANVAS_READER_REQUIRE_WINDOWS: ${{ runner.os == 'Windows' && '1' || '0' }}
run: |
node --test "../../test/*.test.mjs" "../../../../../spec-kit-copilot-sdd/extensions/sdd-canvas/tests/*.test.mjs"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Independent payload and read-only security tests
run: |
npm run test:tools
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Install isolated Chromium on Linux
if: runner.os == 'Linux'
run: |
npm exec -- playwright install --with-deps chromium
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Actual-shell browser journeys
run: |
npm run test:browser
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Upload only synthetic preview screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: canvas-reader-${{ matrix.os }}-screenshots
path: test-results/canvas-reader/**/*.png
if-no-files-found: ignore
retention-days: 14
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
.DS_Store
node_modules/
.playwright-mcp/
dist/
build/
coverage/
test-results/
playwright-report/

# Session artifacts (canvas runtime state, spec-kit init output, css coverage snapshots)
.speckit-wizard/
Expand Down
123 changes: 123 additions & 0 deletions docs/markdown-artifact-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Markdown Artifact Review

The existing Wizard and SDD canvases share a Markdown reader inside their
existing artifact previews. This is an enhancement to those canvases, not a
new plugin, canvas, workflow, or installation method.

## Reading Artifacts

Open an existing artifact using the canvas's **View** control. Markdown
previews include GitHub-flavored tables, lists, task lists, code blocks,
footnotes, and a heading outline. The outline uses a side rail when the
preview has room and a keyboard-accessible drawer in narrower containers.

The artifact selector includes the selected feature's supporting documents,
contracts, checklists, and available project constitution. Wizard also
supports command and template sources already known to its composition.
Relative Markdown links open scoped documents in the same reader. Back and
forward history preserve each document's scroll position for its revision.
Returning to the canvas restores the invoking control and current workflow
state, including progress received while reading.

The source label identifies working-tree content. Existing canvas change
notifications refresh artifact membership and mark changed content; **Refresh**
loads the current revision. Missing, unavailable, oversized, or invalid text
produces an in-preview state without closing the canvas.
If a review context expires, explicit **Refresh** revalidates its scope and
selected document in a new context. Context-bound history starts afresh, and
old clarification bindings are not reused. A document that is no longer in
the authorized scope remains unavailable.

## Existing Workflows

- Canvas identities, agent actions, stage commands, setup, Run/Rerun,
overwrite confirmation, and prerequisite ordering are retained.
- Wizard catalogs, composition, pipeline customization, folder browsing,
and non-Markdown source previews retain their existing entry points.
- SDD feature targeting, constitution, progress, optional quality gates,
new-feature flow, and artifact deep links are retained.
- Existing artifacts can be viewed even when setup or execution is gated.
This intentionally makes SDD feature cards and Wizard artifact navigation
available earlier; it does not enable execution or perform setup.
- Reading, changing documents, following local links, and returning do not
dispatch a workflow, install dependencies, or write project files. The
Wizard's existing startup and dependency bootstrap are unchanged.

Clarification controls remain explicit workflow actions. SDD still confirms
and submits a targeted answer; Wizard retains queued answers and its
apply-and-rerun flow. Reader answers are additionally bound to the current
context, artifact, revision, question, and owning command. Changed questions
must be refreshed and reconfirmed. Code examples and HTML comments do not
become actionable clarification markers. Existing callers without reader
metadata retain their original action contracts.

## Read Boundary

The server derives membership from the existing canvas workspace and selected
feature or composition, not from a browser-supplied absolute path. Additive
`/api/review/*` endpoints use the existing canvas capability plus Host/Origin
checks; legacy endpoint response shapes remain unchanged. Context, artifact,
and pagination identifiers are opaque and instance-bound, not credentials.

Files must be ordinary, bounded, UTF-8 Markdown inside the authorized
workspace. Path traversal, `.git`, symbolic links and junctions, Windows
device aliases and alternate streams are rejected. Revisions hash exact
file bytes; display-only BOM removal never rewrites the source. Errors shown
by the reader are bounded and do not expose file contents or private paths.

Raw HTML is disabled, images are inert placeholders, and task checkboxes
cannot edit documents. Local and external link decisions go through the
host adapter. Unsafe schemes, protocol-relative URLs, and URLs containing
credentials are inert; reading never passively fetches external content.

Limits are 5 MiB per artifact, 200 items per page, 10,000 inspected entries
per discovery pass, and 50 history entries. Reaching the discovery limit
returns a terminal partial result. This reader has no editing, remote
repository discovery, authentication, or cloning feature.

## Maintainer Layout

The build-only package is
[ui/markdown-reader](../plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/ui/markdown-reader/package.json)
under Wizard. It owns rendering, outline, focus, and trusted clarification
presentation. Each canvas keeps its own workflow state and server lifecycle.

Wizard's [review service](../plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/server/artifact-review.mjs)
and browser adapter are canonical. The
[synchronizer](../scripts/canvas-reader/sync-domain.mjs) copies the shared
domain and adapter into SDD, and bundles the clarification parser for Node.
[Asset synchronization](../scripts/canvas-reader/sync-assets.mjs) packages
the reader, dependency notices, and provenance in each plugin independently.
Neither installed plugin requires the sibling checkout or reader build
dependencies. Generated payloads are marked in the repository attributes;
edit their canonical sources and rebuild, not the copies.

## Build And Test

Use Node 24.19.0 and the locked dependencies. From the repository root in
PowerShell:

```powershell
$wizard = "plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas"
$reader = "$wizard/ui/markdown-reader"
npm ci --prefix $wizard --ignore-scripts
npm ci --prefix $reader --ignore-scripts
npm --prefix $reader run typecheck
npm --prefix $reader run lint
npm --prefix $reader test
npm --prefix $reader run build
npm --prefix $reader run verify:package
node --test "$wizard/test/*.test.mjs" "plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/*.test.mjs"
npm --prefix $reader run test:tools
npm --prefix $reader run test:browser
npm --prefix $reader audit --audit-level=low --ignore-scripts
```

The [validation workflow](../.github/workflows/canvas-reader-validation.yml)
runs Windows and Linux checks. CI builds without synchronizing first, so
stale committed payloads fail verification rather than being silently fixed.
Browser tests use Edge on Windows and isolated Chromium on Linux, actual
canvas shells, owned synthetic files, and mocked host sessions. They cover
navigation, responsive layout, clarification, freshness, source preservation,
and independent packaging without executing real workflows. They do not
replace native Copilot App acceptance or real workflow acceptance.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ The agent opens the dashboard in a side panel. See
- **Rendered artifact preview** — view `spec.md`, `plan.md`, `tasks.md`, and the
constitution as formatted headings, lists, code, blockquotes, and tables in a
dedicated full-width canvas view with a **Back to dashboard** control.
The preview also includes a heading outline, related-artifact selection,
history, and revision-aware refresh. See
[Markdown Artifact Review](../../../../docs/markdown-artifact-review.md).
- **Targeted clarification** — `[NEEDS CLARIFICATION: …]` markers in a spec render
a **Clarify** action. The canvas requires an answer in a confirmation dialog
before it sends a validated `clarify` run for that feature to the agent.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import { CONSTITUTION, STAGES, scanFeatures } from "./sdd.mjs";
import { ArtifactReadError } from "./vendor/artifact-read.mjs";
import { createArtifactReviewService } from "./vendor/artifact-review.mjs";
import { scanArtifactCandidates } from "./vendor/artifact-discovery.mjs";

export function sddPrimaryScope(state, selection, workspacePath) {
if (!state?.projectRoot || resolve(state.projectRoot) !== resolve(workspacePath)) {
throw new ArtifactReadError("workspace_unavailable");
}
const stage = selection.stage;
let primary;
let scopeType;
if (stage === CONSTITUTION.key) {
scopeType = "project";
primary = { relativePath: CONSTITUTION.rel.join("/"), label: CONSTITUTION.label,
role: "primary", owningStage: stage, owningCommand: CONSTITUTION.command };
} else {
const feature = selection.feature;
if (typeof feature !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(feature) ||
!state.features?.some((entry) => entry.slug === feature)) return null;
const primaryStage = STAGES.find((entry) => entry.key === stage);
if (!primaryStage) return null;
scopeType = "feature";
primary = { relativePath: `specs/${feature}/${primaryStage.file}`, label: primaryStage.label,
role: "primary", owningStage: stage, owningCommand: primaryStage.command };
}
if (selection.source && selection.source !== primary.relativePath) return null;
const scopeKey = createHash("sha256").update(JSON.stringify([scopeType, selection.feature ?? null, stage])).digest("hex");
const candidates = [primary];
const roots = [];
if (scopeType === "feature") {
roots.push(`specs/${selection.feature}`);
for (const related of STAGES) {
if (related.key === stage) continue;
candidates.push({ relativePath: `specs/${selection.feature}/${related.file}`, label: related.file,
role: "supporting", owningStage: related.key, owningCommand: related.command });
}
candidates.push({ relativePath: CONSTITUTION.rel.join("/"), label: "constitution.md",
role: "supporting", owningStage: "constitution", optional: true });
}
return { scopeType, scopeKey, originStage: stage, originCommand: primary.owningCommand, primary, candidates, roots };
}

export function createSddReviewService({ workspacePath, instanceId, getState = () => scanFeatures(workspacePath) }) {
return createArtifactReviewService({
canvasId: "sdd-canvas", workspacePath, instanceId,
getScope: async (selection) => sddPrimaryScope(await getState(), selection, workspacePath),
discoverCandidates: (scope) => scanArtifactCandidates({ workspacePath, roots: scope.roots, explicit: scope.candidates }),
});
}
Loading