rcf-lite 0.0.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +288 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +69 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +355 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +162 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +216 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +211 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +898 -0
- package/src/core/store/writer.js +1849 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +129 -0
- package/src/finalise/index.js +17 -0
- package/src/finalise/ingest.js +119 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1204 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +9 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +190 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +246 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
package/src/cli/view.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// `rcf view` subcommand handler. Wraps the Phase 3.8 HTTP + SSE server
|
|
2
|
+
// (`src/server/index.js`) behind the unified bin surface. Phase 4 §D23
|
|
3
|
+
// deletes the standalone `bin/rcf-view.js`; every call site becomes
|
|
4
|
+
// `pnpm run rcf view` / `rcf view`, and the pure helpers formerly
|
|
5
|
+
// exported from that bin now live here so the CLI tests can still
|
|
6
|
+
// exercise them without spawning a subprocess.
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { platform } from 'node:process';
|
|
10
|
+
|
|
11
|
+
import { formatErrors } from '#core/errors';
|
|
12
|
+
import { walkTree } from '#core/store';
|
|
13
|
+
import { findProjectRoot } from '../view/index.js';
|
|
14
|
+
import { startServer } from '../server/index.js';
|
|
15
|
+
// Track C+D §9 view-server persistence.
|
|
16
|
+
import {
|
|
17
|
+
startDetached,
|
|
18
|
+
stopDetached,
|
|
19
|
+
statusOfDetached,
|
|
20
|
+
readLogTail,
|
|
21
|
+
} from '../view-supervisor/index.js';
|
|
22
|
+
import { parsePersistUntil } from '../view-supervisor/persist-until.js';
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_PORT = 4373;
|
|
25
|
+
export const SHUTDOWN_BUDGET_MS = 2000;
|
|
26
|
+
|
|
27
|
+
export const HELP = `Usage: rcf view [subverb] [options]
|
|
28
|
+
|
|
29
|
+
Serve the on-disk RCF tree as a live HTML review surface. Runs a
|
|
30
|
+
long-running HTTP + SSE server on 127.0.0.1 that watches rcf/ and pushes
|
|
31
|
+
tree updates to the connected browser tab. No on-disk output; no static
|
|
32
|
+
files are written.
|
|
33
|
+
|
|
34
|
+
Subverbs (spec §9.2):
|
|
35
|
+
rcf view Foreground server (default; lifetime tied to
|
|
36
|
+
the invoking session). Backward-compatible
|
|
37
|
+
with pre-0.7.0 callers.
|
|
38
|
+
rcf view start [--detach|--foreground] [--persist-until <duration|iso>]
|
|
39
|
+
Start the server. --detach forks a supervised
|
|
40
|
+
background process that persists across the
|
|
41
|
+
parent session's death; the manifest carries
|
|
42
|
+
reviewSurface.viewServer for a subsequent
|
|
43
|
+
session to pick up. --persist-until keeps the
|
|
44
|
+
supervisor alive for the named duration
|
|
45
|
+
(e.g. \`4h\`, \`30m\`, \`2h30m\`) or until
|
|
46
|
+
the named ISO timestamp (e.g.
|
|
47
|
+
\`2026-07-31T18:00:00Z\`); unrecognised
|
|
48
|
+
values refuse (exit 2) rather than run
|
|
49
|
+
forever. Default on an interactive TTY is
|
|
50
|
+
--detach; non-interactive callers keep
|
|
51
|
+
--foreground so a script does not orphan a
|
|
52
|
+
process.
|
|
53
|
+
rcf view status [--json] Print the supervisor state:
|
|
54
|
+
running | stale | not-started.
|
|
55
|
+
rcf view stop Send SIGTERM to the supervised process, wait
|
|
56
|
+
for a clean shutdown, and clear the manifest
|
|
57
|
+
record.
|
|
58
|
+
rcf view logs [--tail <n>] Print the supervisor log tail (default 200).
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--port <n> Bind the HTTP server on the given port.
|
|
62
|
+
Precedence: --port beats RCF_VIEW_PORT env
|
|
63
|
+
beats the ${DEFAULT_PORT} default.
|
|
64
|
+
EADDRINUSE is a hard failure (exit 2).
|
|
65
|
+
--strict Startup gate: on boot, walk the tree once; if it
|
|
66
|
+
has structural errors (broken references, schema
|
|
67
|
+
failures) print them and exit 3 without opening
|
|
68
|
+
the HTTP listener. Without --strict, the server
|
|
69
|
+
starts regardless and streams walker errors to the
|
|
70
|
+
client via walker-error SSE events.
|
|
71
|
+
--no-open Do not open the rendered page in a browser
|
|
72
|
+
(auto-open runs by default when stdout is a TTY
|
|
73
|
+
and CI is unset).
|
|
74
|
+
--verbose Log each watch event and each SSE broadcast to
|
|
75
|
+
stderr.
|
|
76
|
+
--help Print this help and exit.
|
|
77
|
+
|
|
78
|
+
Security posture:
|
|
79
|
+
The view server binds 127.0.0.1 only - localhost trust. No CORS,
|
|
80
|
+
no auth, no rate limit. Do not expose it via SSH tunnel or reverse
|
|
81
|
+
proxy without adding an auth layer first.
|
|
82
|
+
|
|
83
|
+
Shutdown:
|
|
84
|
+
Ctrl-C (SIGINT) or SIGTERM triggers a clean shutdown: watcher
|
|
85
|
+
closed, SSE connections drained with a shutdown event, port
|
|
86
|
+
released, 2s force-exit budget. Detached: rcf view stop sends
|
|
87
|
+
SIGTERM to the supervisor and waits for a clean unwind.
|
|
88
|
+
|
|
89
|
+
Exit codes:
|
|
90
|
+
0 normal shutdown
|
|
91
|
+
1 render or runtime failure
|
|
92
|
+
2 usage error (unknown flag, EADDRINUSE, no project root)
|
|
93
|
+
3 validation failure (with --strict, on the initial walk)
|
|
94
|
+
130 SIGINT
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const SUBVERBS = new Set(['start', 'status', 'stop', 'logs']);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse the view subcommand argv. Hand-rolled so the CLI can pass
|
|
101
|
+
* through argv slices without re-tokenising via parseArgs. Kept in the
|
|
102
|
+
* same shape as the Phase 3.8 rcf-view bin for test compatibility.
|
|
103
|
+
*
|
|
104
|
+
* @param {string[]} argv
|
|
105
|
+
* @returns {{ opts: object, errors: string[] }}
|
|
106
|
+
*/
|
|
107
|
+
export function parseArgs(argv) {
|
|
108
|
+
const opts = {
|
|
109
|
+
strict: false,
|
|
110
|
+
verbose: false,
|
|
111
|
+
help: false,
|
|
112
|
+
noOpen: false,
|
|
113
|
+
port: null,
|
|
114
|
+
};
|
|
115
|
+
const errors = [];
|
|
116
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
117
|
+
const arg = argv[i];
|
|
118
|
+
switch (arg) {
|
|
119
|
+
case '--strict':
|
|
120
|
+
opts.strict = true;
|
|
121
|
+
break;
|
|
122
|
+
case '--verbose':
|
|
123
|
+
opts.verbose = true;
|
|
124
|
+
break;
|
|
125
|
+
case '--no-open':
|
|
126
|
+
opts.noOpen = true;
|
|
127
|
+
break;
|
|
128
|
+
case '--help':
|
|
129
|
+
case '-h':
|
|
130
|
+
opts.help = true;
|
|
131
|
+
break;
|
|
132
|
+
case '--port': {
|
|
133
|
+
const next = argv[i + 1];
|
|
134
|
+
if (next === undefined || next.startsWith('--')) {
|
|
135
|
+
errors.push('--port requires a numeric argument');
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const n = Number(next);
|
|
139
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
140
|
+
errors.push(`--port expects an integer in [0, 65535], got ${next}`);
|
|
141
|
+
} else {
|
|
142
|
+
opts.port = n;
|
|
143
|
+
}
|
|
144
|
+
i += 1;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
errors.push(`unknown option: ${arg}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { opts, errors };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve port precedence: --port beats RCF_VIEW_PORT env beats default.
|
|
156
|
+
*
|
|
157
|
+
* @param {number | null} flagPort
|
|
158
|
+
* @param {NodeJS.ProcessEnv} env
|
|
159
|
+
* @returns {{ port: number, errors: string[] }}
|
|
160
|
+
*/
|
|
161
|
+
export function resolvePort(flagPort, env) {
|
|
162
|
+
const errors = [];
|
|
163
|
+
if (typeof flagPort === 'number') return { port: flagPort, errors };
|
|
164
|
+
const raw = env.RCF_VIEW_PORT;
|
|
165
|
+
if (raw !== undefined && raw !== '') {
|
|
166
|
+
const n = Number(raw);
|
|
167
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
168
|
+
errors.push(`RCF_VIEW_PORT expects an integer in [0, 65535], got ${raw}`);
|
|
169
|
+
return { port: DEFAULT_PORT, errors };
|
|
170
|
+
}
|
|
171
|
+
return { port: n, errors };
|
|
172
|
+
}
|
|
173
|
+
return { port: DEFAULT_PORT, errors };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Platform opener command. Kept module-visible so the CLI test suite
|
|
178
|
+
* can assert per-platform selection without spawning a browser.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} plat
|
|
181
|
+
* @param {string} target
|
|
182
|
+
* @returns {{ command: string, args: string[] } | null}
|
|
183
|
+
*/
|
|
184
|
+
export function openerFor(plat, target) {
|
|
185
|
+
if (plat === 'darwin') return { command: 'open', args: [target] };
|
|
186
|
+
if (plat === 'linux') return { command: 'xdg-open', args: [target] };
|
|
187
|
+
if (plat === 'win32') return { command: 'start', args: ['""', target] };
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Auto-open the served URL when we are on a TTY, CI is unset, and
|
|
193
|
+
* --no-open was not passed. Never blocks the parent and never throws;
|
|
194
|
+
* spawn failures fall through to a stderr warning.
|
|
195
|
+
*
|
|
196
|
+
* @param {object} args
|
|
197
|
+
* @returns {boolean}
|
|
198
|
+
*/
|
|
199
|
+
export function maybeAutoOpen({ target, noOpen, stream, env, stderr, spawnFn = spawn, platformName = platform }) {
|
|
200
|
+
if (noOpen) return false;
|
|
201
|
+
if (env.CI) return false;
|
|
202
|
+
if (!stream || !stream.isTTY) return false;
|
|
203
|
+
const opener = openerFor(platformName, target);
|
|
204
|
+
if (!opener) return false;
|
|
205
|
+
try {
|
|
206
|
+
const child = spawnFn(opener.command, opener.args, { detached: true, stdio: 'ignore' });
|
|
207
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
208
|
+
return true;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
stderr.write(`[warn] auto-open: ${err.message}\n`);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Main entry for the `rcf view` subcommand. Mirrors the shape of the
|
|
217
|
+
* old bin/rcf-view.js main().
|
|
218
|
+
*
|
|
219
|
+
* @param {string[]} argv - the argv slice *after* the "view" positional
|
|
220
|
+
* @param {object} [deps]
|
|
221
|
+
* @returns {Promise<number>}
|
|
222
|
+
*/
|
|
223
|
+
export async function main(argv, deps = {}) {
|
|
224
|
+
const env = deps.env ?? process.env;
|
|
225
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
226
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
227
|
+
const onSignal = deps.onSignal ?? ((sig, handler) => process.on(sig, handler));
|
|
228
|
+
|
|
229
|
+
// Track C+D §9: subverb routing. When the first argv token is
|
|
230
|
+
// `start | status | stop | logs`, dispatch to the lifecycle handler
|
|
231
|
+
// and skip the foreground-server path.
|
|
232
|
+
if (argv.length > 0 && SUBVERBS.has(argv[0])) {
|
|
233
|
+
const subverb = argv[0];
|
|
234
|
+
const rest = argv.slice(1);
|
|
235
|
+
return await runSubverb({ subverb, argv: rest, deps: { env, stdout, stderr } });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { opts, errors: argErrors } = parseArgs(argv);
|
|
239
|
+
if (opts.help) {
|
|
240
|
+
stdout.write(HELP);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
if (argErrors.length > 0) {
|
|
244
|
+
for (const msg of argErrors) stderr.write(`[error] usage ${msg}\n`);
|
|
245
|
+
stderr.write(HELP);
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
const { port: resolvedPort, errors: portErrors } = resolvePort(opts.port, env);
|
|
249
|
+
if (portErrors.length > 0) {
|
|
250
|
+
for (const msg of portErrors) stderr.write(`[error] usage ${msg}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
255
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
256
|
+
if (!projectRoot) {
|
|
257
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
258
|
+
stderr.write('Run `rcf init` or create rcf/manifest.json to start.\n');
|
|
259
|
+
return 2;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (opts.strict) {
|
|
263
|
+
const { errors } = await walkTree({ projectRoot });
|
|
264
|
+
if (errors.length > 0) {
|
|
265
|
+
stderr.write(`${formatErrors(errors, { verbose: opts.verbose, strict: true })}\n`);
|
|
266
|
+
return 3;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const logSink = opts.verbose ? (line) => stderr.write(`${line}\n`) : () => {};
|
|
271
|
+
|
|
272
|
+
let server;
|
|
273
|
+
try {
|
|
274
|
+
server = await startServer({
|
|
275
|
+
projectRoot,
|
|
276
|
+
port: resolvedPort,
|
|
277
|
+
log: logSink,
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EADDRINUSE') {
|
|
281
|
+
stderr.write(`[error] usage port ${resolvedPort} is in use (another rcf view process, or a different service).\n`);
|
|
282
|
+
stderr.write('Pass --port <n> or set RCF_VIEW_PORT to pick a free port.\n');
|
|
283
|
+
return 2;
|
|
284
|
+
}
|
|
285
|
+
stderr.write(`[error] ioFailure server failed to start: ${err.message}\n`);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
stdout.write(`rcf view server listening at ${server.url}\n`);
|
|
290
|
+
stdout.write('watching rcf/ - Ctrl-C to shut down\n');
|
|
291
|
+
|
|
292
|
+
maybeAutoOpen({
|
|
293
|
+
target: server.url,
|
|
294
|
+
noOpen: opts.noOpen,
|
|
295
|
+
stream: stdout,
|
|
296
|
+
env,
|
|
297
|
+
stderr,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return new Promise((resolve) => {
|
|
301
|
+
let signalled = false;
|
|
302
|
+
async function handle(sig) {
|
|
303
|
+
if (signalled) return;
|
|
304
|
+
signalled = true;
|
|
305
|
+
stderr.write(`[info] received ${sig}, shutting down\n`);
|
|
306
|
+
let forced = false;
|
|
307
|
+
const forceExit = setTimeout(() => {
|
|
308
|
+
forced = true;
|
|
309
|
+
stderr.write('[warn] shutdown timeout - forcing exit\n');
|
|
310
|
+
resolve(sig === 'SIGINT' ? 130 : 1);
|
|
311
|
+
}, SHUTDOWN_BUDGET_MS);
|
|
312
|
+
if (typeof forceExit.unref === 'function') forceExit.unref();
|
|
313
|
+
try {
|
|
314
|
+
await server.close();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
stderr.write(`[warn] shutdown error: ${err.message}\n`);
|
|
317
|
+
}
|
|
318
|
+
clearTimeout(forceExit);
|
|
319
|
+
if (!forced) resolve(sig === 'SIGINT' ? 130 : 0);
|
|
320
|
+
}
|
|
321
|
+
onSignal('SIGINT', () => { handle('SIGINT'); });
|
|
322
|
+
onSignal('SIGTERM', () => { handle('SIGTERM'); });
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Route the four Track C+D lifecycle subverbs. Kept in the same file
|
|
328
|
+
* as the foreground main so the CLI test surface can exercise both
|
|
329
|
+
* without spawning a subprocess (except for the actual detach test,
|
|
330
|
+
* which spawns intentionally).
|
|
331
|
+
*
|
|
332
|
+
* @param {object} args
|
|
333
|
+
* @param {'start'|'status'|'stop'|'logs'} args.subverb
|
|
334
|
+
* @param {string[]} args.argv
|
|
335
|
+
* @param {{ env: NodeJS.ProcessEnv, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream }} args.deps
|
|
336
|
+
* @returns {Promise<number>}
|
|
337
|
+
*/
|
|
338
|
+
async function runSubverb({ subverb, argv, deps }) {
|
|
339
|
+
const { stdout, stderr, env } = deps;
|
|
340
|
+
const cwd = process.cwd();
|
|
341
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
342
|
+
if (!projectRoot) {
|
|
343
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (subverb === 'status') {
|
|
348
|
+
const useJson = argv.includes('--json');
|
|
349
|
+
const status = await statusOfDetached(projectRoot);
|
|
350
|
+
if (useJson) {
|
|
351
|
+
stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
if (status.state === 'not-started') {
|
|
355
|
+
stdout.write('view server: not-started\n');
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
stdout.write(`view server: ${status.state} (pid ${status.pid ?? '?'})\n`);
|
|
359
|
+
if (status.url) stdout.write(` url: ${status.url}\n`);
|
|
360
|
+
if (status.lastHeartbeatAt) stdout.write(` lastHeartbeatAt: ${status.lastHeartbeatAt}\n`);
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (subverb === 'stop') {
|
|
365
|
+
const outcome = await stopDetached(projectRoot);
|
|
366
|
+
if (outcome.stopped) {
|
|
367
|
+
stdout.write(`view server stopped (pid ${outcome.pid})\n`);
|
|
368
|
+
} else if (outcome.pid !== null) {
|
|
369
|
+
stdout.write(`view server: pid ${outcome.pid} was not alive; cleared stale record\n`);
|
|
370
|
+
} else {
|
|
371
|
+
stdout.write('view server: not-started (no record to clear)\n');
|
|
372
|
+
}
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (subverb === 'logs') {
|
|
377
|
+
const tailIdx = argv.indexOf('--tail');
|
|
378
|
+
const tail = tailIdx >= 0 ? Number.parseInt(argv[tailIdx + 1] ?? '', 10) : 200;
|
|
379
|
+
const lines = await readLogTail(projectRoot, Number.isFinite(tail) ? tail : 200);
|
|
380
|
+
if (lines.length === 0) {
|
|
381
|
+
stdout.write('view server: no log entries yet\n');
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
for (const line of lines) stdout.write(`${line}\n`);
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// subverb === 'start'
|
|
389
|
+
let detach = null;
|
|
390
|
+
let persistUntilRaw = null;
|
|
391
|
+
let port = null;
|
|
392
|
+
const opts = argv.slice();
|
|
393
|
+
while (opts.length > 0) {
|
|
394
|
+
const flag = opts.shift();
|
|
395
|
+
if (flag === '--detach') detach = true;
|
|
396
|
+
else if (flag === '--foreground') detach = false;
|
|
397
|
+
else if (flag === '--persist-until') persistUntilRaw = opts.shift() ?? null;
|
|
398
|
+
else if (flag === '--port') {
|
|
399
|
+
const raw = opts.shift();
|
|
400
|
+
const n = Number.parseInt(raw ?? '', 10);
|
|
401
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
402
|
+
stderr.write(`[error] usage view start: --port expects an integer 0..65535, got ${raw}\n`);
|
|
403
|
+
return 2;
|
|
404
|
+
}
|
|
405
|
+
port = n;
|
|
406
|
+
} else if (flag === '--help') {
|
|
407
|
+
stdout.write(HELP);
|
|
408
|
+
return 0;
|
|
409
|
+
} else {
|
|
410
|
+
stderr.write(`[error] usage view start: unknown option ${flag}\n`);
|
|
411
|
+
return 2;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Parse --persist-until if supplied. The supervisor expects an ISO
|
|
415
|
+
// timestamp (its Date.parse silently no-ops on anything else); we
|
|
416
|
+
// accept the spec §9.2 sample form `4h` here and normalise to ISO
|
|
417
|
+
// before dispatch. Unrecognised input refuses cleanly (exit 2)
|
|
418
|
+
// instead of running forever.
|
|
419
|
+
let persistUntil = null;
|
|
420
|
+
if (persistUntilRaw !== null) {
|
|
421
|
+
const parsed = parsePersistUntil(persistUntilRaw);
|
|
422
|
+
if (!parsed.ok) {
|
|
423
|
+
stderr.write(`[error] usage view start: ${parsed.error}\n`);
|
|
424
|
+
return 2;
|
|
425
|
+
}
|
|
426
|
+
persistUntil = parsed.iso;
|
|
427
|
+
}
|
|
428
|
+
const envPort = Number.parseInt(env.RCF_VIEW_PORT ?? '', 10);
|
|
429
|
+
const resolvedPort = port ?? (Number.isFinite(envPort) && envPort >= 0 ? envPort : DEFAULT_PORT);
|
|
430
|
+
// Detach default: interactive TTY implies detach; script/CI keeps foreground.
|
|
431
|
+
const shouldDetach = detach === null ? Boolean(process.stdout.isTTY && process.stdin.isTTY) : detach;
|
|
432
|
+
if (!shouldDetach) {
|
|
433
|
+
// Fall through to the classic foreground behaviour, passing through
|
|
434
|
+
// any port/verbose flags via the standard main-loop arg surface.
|
|
435
|
+
const passArgs = [];
|
|
436
|
+
if (port !== null) { passArgs.push('--port', String(resolvedPort)); }
|
|
437
|
+
return await main(passArgs, { stdout, stderr, env });
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const started = await startDetached({ projectRoot, port: resolvedPort, persistUntil });
|
|
441
|
+
if (started.alreadyRunning) {
|
|
442
|
+
stdout.write(`view server already running at ${started.url} (pid ${started.pid})\n`);
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
stdout.write(`view server started detached at ${started.url} (pid ${started.pid})\n`);
|
|
446
|
+
if (persistUntil) stdout.write(` persistUntil: ${persistUntil}\n`);
|
|
447
|
+
return 0;
|
|
448
|
+
} catch (err) {
|
|
449
|
+
stderr.write(`[error] ioFailure view start --detach: ${err.message}\n`);
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceReqShape": "auth",
|
|
3
|
+
"specSource": "elicitation-and-playbook-hardening-0.7.0-spec.md §5.2.3",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"baselineKey": "auth.htmlLoginPage",
|
|
7
|
+
"canonicalText": "given the operator has not opted out via preflight rec-3 (Track A §4.6 seam), when `GET /login` is requested, then the response is a 200 with `content-type: text/html; charset=utf-8` and a form containing the fields the auth flow requires (email, password, or magic-link email box per the auth mechanism).",
|
|
8
|
+
"given": "the operator has not opted out via preflight rec-3 (Track A §4.6 seam)",
|
|
9
|
+
"when": "`GET /login` is requested",
|
|
10
|
+
"then": "the response is a 200 with `content-type: text/html; charset=utf-8` and a form containing the fields the auth flow requires (email, password, or magic-link email box per the auth mechanism)",
|
|
11
|
+
"notes": null,
|
|
12
|
+
"testable": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"baselineKey": "auth.postLogout",
|
|
16
|
+
"canonicalText": "given a signed-in session, when `POST /logout` is invoked, then the session cookie is cleared (Set-Cookie with an expiry in the past), the response is 302 to `/login` (or the project-declared post-logout route), and subsequent requests to protected routes return 401 or 302 as the REQ defines.",
|
|
17
|
+
"given": "a signed-in session",
|
|
18
|
+
"when": "`POST /logout` is invoked",
|
|
19
|
+
"then": "the session cookie is cleared (Set-Cookie with an expiry in the past), the response is 302 to `/login` (or the project-declared post-logout route), and subsequent requests to protected routes return 401 or 302 as the REQ defines",
|
|
20
|
+
"notes": null,
|
|
21
|
+
"testable": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"baselineKey": "auth.sessionCookieShape",
|
|
25
|
+
"canonicalText": "given any signed-in response, when the auth cookie is set, then the cookie carries `HttpOnly`, `Secure` (when the runtime is HTTPS), `SameSite=Lax` (or the project-declared value), and a documented expiry.",
|
|
26
|
+
"given": "any signed-in response",
|
|
27
|
+
"when": "the auth cookie is set",
|
|
28
|
+
"then": "the cookie carries `HttpOnly`, `Secure` (when the runtime is HTTPS), `SameSite=Lax` (or the project-declared value), and a documented expiry",
|
|
29
|
+
"notes": null,
|
|
30
|
+
"testable": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"baselineKey": "auth.emptyTokenRejection",
|
|
34
|
+
"canonicalText": "given the auth mechanism uses tokens or magic-links, when a request presents an empty or malformed token, then the response is 400, 401, or a redirect to `/login` with an error parameter; NEVER a 200 that grants a session.",
|
|
35
|
+
"given": "the auth mechanism uses tokens or magic-links",
|
|
36
|
+
"when": "a request presents an empty or malformed token",
|
|
37
|
+
"then": "the response is 400, 401, or a redirect to `/login` with an error parameter; NEVER a 200 that grants a session",
|
|
38
|
+
"notes": null,
|
|
39
|
+
"testable": true
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceReqShape": "httpApi",
|
|
3
|
+
"specSource": "elicitation-and-playbook-hardening-0.7.0-spec.md §5.2.2",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"baselineKey": "httpApi.errorEnvelope",
|
|
7
|
+
"canonicalText": "given any endpoint, when the endpoint returns a non-2xx response, then the body is a JSON object with fields `error.code` (string, machine-readable), `error.message` (string, human-readable), and `error.requestId` (string, correlatable to logs).",
|
|
8
|
+
"given": "any endpoint",
|
|
9
|
+
"when": "the endpoint returns a non-2xx response",
|
|
10
|
+
"then": "the body is a JSON object with fields `error.code` (string, machine-readable), `error.message` (string, human-readable), and `error.requestId` (string, correlatable to logs)",
|
|
11
|
+
"notes": null,
|
|
12
|
+
"testable": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"baselineKey": "httpApi.contentType",
|
|
16
|
+
"canonicalText": "given any endpoint returning a response body, when the response is emitted, then `content-type: application/json; charset=utf-8` (or the project-declared content type in the REQ's `rationale`) is set explicitly on the response.",
|
|
17
|
+
"given": "any endpoint returning a response body",
|
|
18
|
+
"when": "the response is emitted",
|
|
19
|
+
"then": "`content-type: application/json; charset=utf-8` (or the project-declared content type in the REQ's `rationale`) is set explicitly on the response",
|
|
20
|
+
"notes": null,
|
|
21
|
+
"testable": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"baselineKey": "httpApi.authBoundary",
|
|
25
|
+
"canonicalText": "given an endpoint the REQ marks as protected, when a request without valid credentials is received, then the response is 401 with the standard error envelope (never 200 with a generic \"not found\" that leaks the endpoint's existence in some sessions and hides it in others).",
|
|
26
|
+
"given": "an endpoint the REQ marks as protected",
|
|
27
|
+
"when": "a request without valid credentials is received",
|
|
28
|
+
"then": "the response is 401 with the standard error envelope (never 200 with a generic \"not found\" that leaks the endpoint's existence in some sessions and hides it in others)",
|
|
29
|
+
"notes": null,
|
|
30
|
+
"testable": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"baselineKey": "httpApi.rateLimitDisclosure",
|
|
34
|
+
"canonicalText": "given a REQ that opts into rate limiting (via a rate-limit field on the REQ or explicit AC), when a request is throttled, then the response is 429 with `retry-after` set to the number of seconds until retry; when a request is NOT throttled but rate limits exist, then `x-ratelimit-remaining` is present on the response. Opting out is the norm for REQs with no rate limiting.",
|
|
35
|
+
"given": "a REQ that opts into rate limiting (via a rate-limit field on the REQ or explicit AC)",
|
|
36
|
+
"when": "a request is throttled",
|
|
37
|
+
"then": "the response is 429 with `retry-after` set to the number of seconds until retry; when a request is NOT throttled but rate limits exist, then `x-ratelimit-remaining` is present on the response",
|
|
38
|
+
"notes": "Opting out is the norm for REQs with no rate limiting.",
|
|
39
|
+
"testable": true
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceReqShape": "notifications",
|
|
3
|
+
"specSource": "elicitation-and-playbook-hardening-0.7.0-spec.md §5.2.5",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"baselineKey": "notifications.deliveryObservability",
|
|
7
|
+
"canonicalText": "given a notification is dispatched, when dispatch succeeds, then an observable delivery record is produced (a log line, a database row, an admin-UI entry) with fields recipient, channel, status, and timestamp.",
|
|
8
|
+
"given": "a notification is dispatched",
|
|
9
|
+
"when": "dispatch succeeds",
|
|
10
|
+
"then": "an observable delivery record is produced (a log line, a database row, an admin-UI entry) with fields recipient, channel, status, and timestamp",
|
|
11
|
+
"notes": null,
|
|
12
|
+
"testable": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"baselineKey": "notifications.retryOrDeadLetter",
|
|
16
|
+
"canonicalText": "given a notification dispatch fails and the channel supports retry, when the failure is transient, then the dispatcher retries per a documented policy; when retries are exhausted, the notification lands in a dead-letter store the operator can inspect.",
|
|
17
|
+
"given": "a notification dispatch fails and the channel supports retry",
|
|
18
|
+
"when": "the failure is transient",
|
|
19
|
+
"then": "the dispatcher retries per a documented policy; when retries are exhausted, the notification lands in a dead-letter store the operator can inspect",
|
|
20
|
+
"notes": null,
|
|
21
|
+
"testable": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"baselineKey": "notifications.channelIdentification",
|
|
25
|
+
"canonicalText": "given a REQ that names a channel (email, SMS, webhook), when a notification is dispatched, then the payload identifies the sender to the recipient (email `from` address, SMS sender ID, webhook `x-source` header) per project convention.",
|
|
26
|
+
"given": "a REQ that names a channel (email, SMS, webhook)",
|
|
27
|
+
"when": "a notification is dispatched",
|
|
28
|
+
"then": "the payload identifies the sender to the recipient (email `from` address, SMS sender ID, webhook `x-source` header) per project convention",
|
|
29
|
+
"notes": null,
|
|
30
|
+
"testable": true
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceReqShape": "persistence",
|
|
3
|
+
"specSource": "elicitation-and-playbook-hardening-0.7.0-spec.md §5.2.4",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"baselineKey": "persistence.crashSafety",
|
|
7
|
+
"canonicalText": "given a write completes and the process is killed unexpectedly, when the process restarts and the data is read back, then the write is present and identical to what was written.",
|
|
8
|
+
"given": "a write completes and the process is killed unexpectedly",
|
|
9
|
+
"when": "the process restarts and the data is read back",
|
|
10
|
+
"then": "the write is present and identical to what was written",
|
|
11
|
+
"notes": null,
|
|
12
|
+
"testable": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"baselineKey": "persistence.schemaMigration",
|
|
16
|
+
"canonicalText": "given the storage schema evolves across versions, when the app starts against an older schema, then the app either applies a documented migration and starts, or refuses to start with a clear error naming the schema version mismatch. Silent schema drift is the failure mode this AC exists to prevent.",
|
|
17
|
+
"given": "the storage schema evolves across versions",
|
|
18
|
+
"when": "the app starts against an older schema",
|
|
19
|
+
"then": "the app either applies a documented migration and starts, or refuses to start with a clear error naming the schema version mismatch",
|
|
20
|
+
"notes": "Silent schema drift is the failure mode this AC exists to prevent.",
|
|
21
|
+
"testable": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"baselineKey": "persistence.backupObservable",
|
|
25
|
+
"canonicalText": "given a REQ that mentions backups or retention, when a backup runs, then the backup produces an observable artefact (a file, a log entry, a metrics counter) that a monitoring surface can read. Opt out where backups are entirely out of scope for the REQ.",
|
|
26
|
+
"given": "a REQ that mentions backups or retention",
|
|
27
|
+
"when": "a backup runs",
|
|
28
|
+
"then": "the backup produces an observable artefact (a file, a log entry, a metrics counter) that a monitoring surface can read",
|
|
29
|
+
"notes": "Opt out where backups are entirely out of scope for the REQ.",
|
|
30
|
+
"testable": true
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceReqShape": "webUi",
|
|
3
|
+
"specSource": "elicitation-and-playbook-hardening-0.7.0-spec.md §5.2.1",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"baselineKey": "webUi.sharedNav",
|
|
7
|
+
"canonicalText": "given the user is signed in, when any authenticated route is loaded, then the response HTML renders the shared nav element (declared by `uiBaseline.defaults.sharedLayoutModule`) with `aria-current=\"page\"` on the nav link whose href matches the current route.",
|
|
8
|
+
"given": "the user is signed in",
|
|
9
|
+
"when": "any authenticated route is loaded",
|
|
10
|
+
"then": "the response HTML renders the shared nav element (declared by `uiBaseline.defaults.sharedLayoutModule`) with `aria-current=\"page\"` on the nav link whose href matches the current route",
|
|
11
|
+
"notes": null,
|
|
12
|
+
"testable": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"baselineKey": "webUi.themeToggle",
|
|
16
|
+
"canonicalText": "given a first-load with no theme cookie set, when the page renders, then `<html data-theme=\"light\">` is present; AND a user-visible theme toggle control is rendered on every authenticated route; AND clicking the toggle persists the choice via cookie or local storage for subsequent requests. The default-light case is hardcoded here per Track B §8.3 `themeDefaultsToLight` invariant; projects that opt into a dark default supersede this AC via a `baselineAcOptOuts[]` entry (§3.3), which Track B's `themeDefaultsToLight` tolerates by design.",
|
|
17
|
+
"given": "a first-load with no theme cookie set",
|
|
18
|
+
"when": "the page renders",
|
|
19
|
+
"then": "`<html data-theme=\"light\">` is present; AND a user-visible theme toggle control is rendered on every authenticated route; AND clicking the toggle persists the choice via cookie or local storage for subsequent requests",
|
|
20
|
+
"notes": "The default-light case is hardcoded here per Track B §8.3 `themeDefaultsToLight` invariant; projects that opt into a dark default supersede this AC via a `baselineAcOptOuts[]` entry (§3.3), which Track B's `themeDefaultsToLight` tolerates by design.",
|
|
21
|
+
"testable": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"baselineKey": "webUi.contrastFloor",
|
|
25
|
+
"canonicalText": "given the shipped palette, when the contrast test declared at `designStage.themeAndA11y.contrastTestPath` (Track B §3.1) runs, then every text-on-background pair meets WCAG AA (4.5 for text, 3.0 for large text and UI components); AND the test was authored before the palette (per Track B §7 mandate 10, attested via the boolean at `designStage.themeAndA11y.contrastTestAuthoredBeforePalette`). The runtime chain check that couples this AC to the shipped chain is Track B §8.3 `contrastTestExists`; this AC is the elicitation-time expression of that check.",
|
|
26
|
+
"given": "the shipped palette",
|
|
27
|
+
"when": "the contrast test declared at `designStage.themeAndA11y.contrastTestPath` (Track B §3.1) runs",
|
|
28
|
+
"then": "every text-on-background pair meets WCAG AA (4.5 for text, 3.0 for large text and UI components); AND the test was authored before the palette (per Track B §7 mandate 10, attested via the boolean at `designStage.themeAndA11y.contrastTestAuthoredBeforePalette`)",
|
|
29
|
+
"notes": "The runtime chain check that couples this AC to the shipped chain is Track B §8.3 `contrastTestExists`; this AC is the elicitation-time expression of that check.",
|
|
30
|
+
"testable": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"baselineKey": "webUi.focusRings",
|
|
34
|
+
"canonicalText": "given any interactive element (button, input, link), when the element receives keyboard focus, then a visible focus ring is rendered in the current theme.",
|
|
35
|
+
"given": "any interactive element (button, input, link)",
|
|
36
|
+
"when": "the element receives keyboard focus",
|
|
37
|
+
"then": "a visible focus ring is rendered in the current theme",
|
|
38
|
+
"notes": null,
|
|
39
|
+
"testable": true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"baselineKey": "webUi.signedInAffordance",
|
|
43
|
+
"canonicalText": "given the user is signed in and the request carries the auth cookie, when any authenticated route is loaded, then the response HTML contains a visible signed-in-as affordance (per Track B `browserVerification` invariant `signedInAsAffordance`).",
|
|
44
|
+
"given": "the user is signed in and the request carries the auth cookie",
|
|
45
|
+
"when": "any authenticated route is loaded",
|
|
46
|
+
"then": "the response HTML contains a visible signed-in-as affordance (per Track B `browserVerification` invariant `signedInAsAffordance`)",
|
|
47
|
+
"notes": null,
|
|
48
|
+
"testable": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"baselineKey": "webUi.oneJourneyOrMore",
|
|
52
|
+
"canonicalText": "given the story defines a user-facing outcome, when the Design substage runs (Track B §5), then `designStage.journeys[]` contains at least one entry whose `goal` traces to this AC's `then`. Track B's Design gate enforces journey PRESENCE via `designStageComplete` (§5.2, §5.5 refuse when the artefact is absent or empty); this AC ensures the story-level goal-to-AC trace is at least declarative on the AC so a reviewer can confirm the trace by reading the journey's `goal` alongside the AC's `then`. The trace itself is a review touchpoint, not a machine-checked chain assertion.",
|
|
53
|
+
"given": "the story defines a user-facing outcome",
|
|
54
|
+
"when": "the Design substage runs (Track B §5)",
|
|
55
|
+
"then": "`designStage.journeys[]` contains at least one entry whose `goal` traces to this AC's `then`",
|
|
56
|
+
"notes": "Track B's Design gate enforces journey PRESENCE via `designStageComplete` (§5.2, §5.5 refuse when the artefact is absent or empty); this AC ensures the story-level goal-to-AC trace is at least declarative on the AC so a reviewer can confirm the trace by reading the journey's `goal` alongside the AC's `then`. The trace itself is a review touchpoint, not a machine-checked chain assertion.",
|
|
57
|
+
"testable": true
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|