diff --git a/.changeset/1139-phase6-capstone-conformance.md b/.changeset/1139-phase6-capstone-conformance.md new file mode 100644 index 000000000..27bc53be6 --- /dev/null +++ b/.changeset/1139-phase6-capstone-conformance.md @@ -0,0 +1,8 @@ +--- +"@opengsd/gsd-core": patch +type: Changed +issue: 1139 +pr: 1158 +--- + +Added ADR-857 Phase 6 capstone conformance coverage so migrated Capability activation keys cannot be read directly from host loop workflows, Capability-owned config keys stay out of the central schema, and the host loop workflow size budgets remain documented. The verify-work UI automation preflight now resolves UI activation through the Capability hook registry instead of reading `workflow.ui_phase` directly. diff --git a/docs/how-to/develop-a-capability.md b/docs/how-to/develop-a-capability.md index 89507d196..498f00412 100644 --- a/docs/how-to/develop-a-capability.md +++ b/docs/how-to/develop-a-capability.md @@ -225,9 +225,12 @@ Run the smallest affected tests first, then the full suite before opening a read ```bash node --test tests/capability-registry.test.cjs node --test tests/phase6-planning-capabilities.test.cjs +node --test tests/phase6-capstone-conformance.test.cjs npm test ``` +Run the Phase 6 capstone test whenever a Capability adds a `when` key or moves activation logic in a host workflow. It checks that migrated activation keys are resolved through Capability hooks or state, and that Capability-owned config keys stay out of the central schema. If a host workflow must keep core behaviour behind a `workflow.*` key, document why it is core rather than Capability-owned before adding it. + ## Keep the docs with the slice Every Phase 6 slice that changes capability behaviour must update the relevant docs in the same PR. Use this manual for developer-facing Capability authoring facts, use how-to guides for task flows, and use ADRs only for decisions and trade-offs. diff --git a/gsd-core/workflows/verify-work.md b/gsd-core/workflows/verify-work.md index 307093842..8dc00037a 100644 --- a/gsd-core/workflows/verify-work.md +++ b/gsd-core/workflows/verify-work.md @@ -108,16 +108,17 @@ Continue to `create_uat_file`. **Automated UI Verification (when Playwright-MCP is available)** -Before running manual UAT, check whether this phase has a UI component and whether -`mcp__playwright__*` or `mcp__puppeteer__*` tools are available in the current session. +Before UAT, check UI capability activation and whether Playwright/Puppeteer MCP tools are available. ```bash -UI_PHASE_FLAG=$(gsd_run query config-get workflow.ui_phase --raw 2>/dev/null || echo "true") +PLAN_HOOKS_JSON=$(gsd_run loop render-hooks plan:pre --raw) UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1) ``` +Set `UI_PHASE_ACTIVE=true` when `PLAN_HOOKS_JSON.activeHooks` contains an active `ui` step hook. + **If Playwright-MCP tools are available in this session (`mcp__playwright__*` tools -respond to tool calls) AND (`UI_PHASE_FLAG` is `true` OR `UI_SPEC_FILE` is non-empty):** +respond to tool calls) AND (`UI_PHASE_ACTIVE` is `true` OR `UI_SPEC_FILE` is non-empty):** For each UI checkpoint listed in the phase's UI-SPEC.md (or inferred from SUMMARY.md): diff --git a/tests/phase6-capstone-conformance.test.cjs b/tests/phase6-capstone-conformance.test.cjs new file mode 100644 index 000000000..60069341d --- /dev/null +++ b/tests/phase6-capstone-conformance.test.cjs @@ -0,0 +1,108 @@ +// allow-test-rule: source-text-is-the-product +'use strict'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const HOST_LOOP_FILES = [ + 'gsd-core/workflows/plan-phase.md', + 'gsd-core/workflows/execute-phase.md', + 'gsd-core/workflows/verify-work.md', + 'gsd-core/workflows/ship.md', +]; + +const CORE_SUBSTRATE_TERMS = [ + 'Verification substrate', + 'verifier↔predicate contract', + 'Probe Core Module', + 'Edge Probe Module', +]; + +const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); +const { isCentralConfigKey } = require('../gsd-core/bin/lib/config-schema.cjs'); + +function readRepoFile(relativePath) { + return fs.readFileSync(path.join(ROOT, relativePath), 'utf8'); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function activeWhenKeys() { + const keys = new Set(); + for (const cap of Object.values(registry.capabilities)) { + for (const group of ['steps', 'gates', 'contributions']) { + for (const hook of cap[group] || []) { + if (hook.when) keys.add(hook.when); + } + } + } + return [...keys].sort(); +} + +describe('ADR-857 Phase 6 capstone conformance (#1139)', () => { + test('first-party optional feature capabilities are declared in the generated registry', () => { + const expectedFeatureCapabilities = [ + 'ai-integration', + 'audit', + 'code-review', + 'graphify', + 'intel', + 'nyquist', + 'pattern-mapper', + 'research', + 'security', + 'ui', + ]; + + for (const capId of expectedFeatureCapabilities) { + assert.equal(registry.capabilities[capId]?.role, 'feature', `${capId} must be a feature Capability`); + } + }); + + test('core verification substrate is documented as deliberately not capability-owned', () => { + const context = readRepoFile('CONTEXT.md'); + for (const term of CORE_SUBSTRATE_TERMS) { + assert.match(context, new RegExp(escapeRegExp(term)), `${term} must be documented in CONTEXT.md`); + } + }); + + test('host loop files do not read capability hook activation keys directly', () => { + const forbiddenKeys = activeWhenKeys(); + assert.ok(forbiddenKeys.length > 0, 'registry must expose hook activation keys'); + + for (const relativePath of HOST_LOOP_FILES) { + const content = readRepoFile(relativePath); + for (const key of forbiddenKeys) { + assert.doesNotMatch( + content, + new RegExp(`\\bconfig-get\\s+${escapeRegExp(key)}\\b`), + `${relativePath} must resolve ${key} through Capability hooks/state, not direct config-get`, + ); + } + } + }); + + test('capability-owned config keys are not reintroduced into the central schema', () => { + for (const key of Object.keys(registry.configKeys).sort()) { + assert.equal( + isCentralConfigKey(key), + false, + `${key} is owned by capability ${registry.configKeys[key]} and must stay out of central config schema`, + ); + } + }); + + test('host loop workflow files have committed byte budgets', () => { + const baseline = JSON.parse(readRepoFile('tests/workflow-size-baseline.json')); + for (const relativePath of HOST_LOOP_FILES) { + const fileName = path.basename(relativePath); + assert.equal(typeof baseline[fileName], 'number', `${fileName} must have a workflow-size baseline`); + assert.ok(baseline[fileName] > 0, `${fileName} baseline must be positive`); + } + }); +}); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 7f7eb2d11..354053def 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -86,5 +86,5 @@ "update.md": 21053, "validate-phase.md": 10745, "verify-phase.md": 28430, - "verify-work.md": 31160 + "verify-work.md": 31157 }