rcf-lite 0.0.1 → 0.8.0
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 +344 -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 +71 -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/admissibility/enforce.js +142 -0
- package/src/admissibility/index.js +8 -0
- package/src/admissibility/markers.js +104 -0
- package/src/admissibility/scope-lint.js +163 -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 +358 -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 +168 -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 +221 -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 +214 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +944 -0
- package/src/core/store/writer.js +1879 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +151 -0
- package/src/finalise/index.js +31 -0
- package/src/finalise/ingest.js +160 -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 +1212 -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 +13 -0
- package/src/query/refuse-on-admissibility.js +73 -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/ruleset/index.js +140 -0
- package/src/ruleset/ruleset.json +146 -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 +221 -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 +313 -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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Strand 2: seed writer for `rcf/knowledge/` (0.6.0 spec §3). Init
|
|
2
|
+
// writes four files unconditionally on a fresh scaffold; on a re-run,
|
|
3
|
+
// any managed file that already exists is left byte-identical. The
|
|
4
|
+
// seed is convention-only in v1 (no CLI verb, no retrieval machinery);
|
|
5
|
+
// what ships is the README that teaches the convention and a stub
|
|
6
|
+
// INDEX.md for human bullets.
|
|
7
|
+
//
|
|
8
|
+
// Idempotency contract (§3.3): the four files above are the ones that
|
|
9
|
+
// count as "managed by init". Any file the operator drops under
|
|
10
|
+
// `rcf/knowledge/` stays as-is. If the whole tree is missing on a
|
|
11
|
+
// re-run, init re-seeds it (a repo that pre-dates 0.6.0 gets the
|
|
12
|
+
// convention retroactively without needing a hand-edit).
|
|
13
|
+
|
|
14
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
/** Canonical README (spec §3.4). Verbatim. */
|
|
18
|
+
export const KNOWLEDGE_README = `# Knowledge
|
|
19
|
+
|
|
20
|
+
This directory is the project's memory. Everything an agent working on
|
|
21
|
+
this repo should not have to relearn from scratch belongs here.
|
|
22
|
+
|
|
23
|
+
## The convention
|
|
24
|
+
|
|
25
|
+
- **\`notes/\`**: internal facts. Decisions, gotchas, runtime facts,
|
|
26
|
+
the small things that always cost time to rediscover ("the CI matrix
|
|
27
|
+
uses Node 22 not 24", "the pnpm store path on this machine is
|
|
28
|
+
non-default", "the local Postgres is on port 5433 not 5432"). Written
|
|
29
|
+
for future agents and future you, not for public readers.
|
|
30
|
+
- **\`docs/\`**: user-facing prose the project might surface elsewhere.
|
|
31
|
+
Design notes the operator wants tidy, sections that might land in a
|
|
32
|
+
README or a spec, prose intended for a wider audience.
|
|
33
|
+
|
|
34
|
+
## The rules
|
|
35
|
+
|
|
36
|
+
1. **One topic per file.** The filename is the topic
|
|
37
|
+
(\`notes/ci-node-version.md\`, not \`notes/general.md\`). One paragraph
|
|
38
|
+
is enough. An empty file that only carries a pointer to somewhere
|
|
39
|
+
else is fine. A file called \`misc.md\` or \`general.md\` is not.
|
|
40
|
+
2. **Write on learn.** If this session established a fact the next
|
|
41
|
+
session should not have to rediscover, write it here before the
|
|
42
|
+
session ends.
|
|
43
|
+
3. **Grep before asking.** Before asking the stakeholder a question,
|
|
44
|
+
\`rg -n '<topic>' rcf/knowledge/\` to see whether the answer is
|
|
45
|
+
already here.
|
|
46
|
+
|
|
47
|
+
## \`INDEX.md\`
|
|
48
|
+
|
|
49
|
+
\`INDEX.md\` is a human table of contents. Add a bullet when you add a
|
|
50
|
+
file. Keep it grouped by area if it grows. Nobody scans it
|
|
51
|
+
programmatically in v1; it is for the person landing on the repo cold.
|
|
52
|
+
|
|
53
|
+
## What this is not
|
|
54
|
+
|
|
55
|
+
This is not a knowledge graph. There is no CLI verb, no indexer, no
|
|
56
|
+
vector search. If a project needs one, that is a v2 decision, not a v1
|
|
57
|
+
default. The convention above is deliberately cheap. The value is in
|
|
58
|
+
the discipline of using it.
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
/** Canonical INDEX.md stub (spec §3.4). Verbatim. */
|
|
62
|
+
export const KNOWLEDGE_INDEX = `# Knowledge index
|
|
63
|
+
|
|
64
|
+
The human table of contents for \`rcf/knowledge/\`. Add a bullet when you
|
|
65
|
+
add a file. Keep it grouped by area if it grows.
|
|
66
|
+
|
|
67
|
+
## Notes
|
|
68
|
+
|
|
69
|
+
_(none yet)_
|
|
70
|
+
|
|
71
|
+
## Docs
|
|
72
|
+
|
|
73
|
+
_(none yet)_
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Absolute path helpers for the seed files.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} projectRoot
|
|
80
|
+
* @returns {{ dir: string, readme: string, index: string, notesKeep: string, docsKeep: string }}
|
|
81
|
+
*/
|
|
82
|
+
export function knowledgePaths(projectRoot) {
|
|
83
|
+
const dir = join(projectRoot, 'rcf', 'knowledge');
|
|
84
|
+
return {
|
|
85
|
+
dir,
|
|
86
|
+
readme: join(dir, 'README.md'),
|
|
87
|
+
index: join(dir, 'INDEX.md'),
|
|
88
|
+
notesKeep: join(dir, 'notes', '.gitkeep'),
|
|
89
|
+
docsKeep: join(dir, 'docs', '.gitkeep'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function writeIfMissing(path, contents) {
|
|
94
|
+
try {
|
|
95
|
+
await readFile(path, 'utf8');
|
|
96
|
+
return { path, action: /** @type {'kept'} */ ('kept') };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') throw err;
|
|
99
|
+
}
|
|
100
|
+
await mkdir(dirname(path), { recursive: true });
|
|
101
|
+
await writeFile(path, contents, 'utf8');
|
|
102
|
+
return { path, action: /** @type {'created'} */ ('created') };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Seed the four managed files under `rcf/knowledge/`. Any file already
|
|
107
|
+
* present is left byte-identical (AC-2.2). If the whole tree is missing
|
|
108
|
+
* on a re-run, all four are recreated (AC-2.3).
|
|
109
|
+
*
|
|
110
|
+
* @param {object} args
|
|
111
|
+
* @param {string} args.projectRoot
|
|
112
|
+
* @returns {Promise<{ writes: Array<{ file: string, action: 'created' | 'kept' }> }>}
|
|
113
|
+
*/
|
|
114
|
+
export async function writeKnowledgeSeed({ projectRoot }) {
|
|
115
|
+
const paths = knowledgePaths(projectRoot);
|
|
116
|
+
const results = [
|
|
117
|
+
{ file: 'rcf/knowledge/README.md', ...(await writeIfMissing(paths.readme, KNOWLEDGE_README)) },
|
|
118
|
+
{ file: 'rcf/knowledge/INDEX.md', ...(await writeIfMissing(paths.index, KNOWLEDGE_INDEX)) },
|
|
119
|
+
{ file: 'rcf/knowledge/notes/.gitkeep', ...(await writeIfMissing(paths.notesKeep, '')) },
|
|
120
|
+
{ file: 'rcf/knowledge/docs/.gitkeep', ...(await writeIfMissing(paths.docsKeep, '')) },
|
|
121
|
+
];
|
|
122
|
+
return { writes: results.map((r) => ({ file: r.file, action: r.action })) };
|
|
123
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Generic "managed block in file X" primitive (0.6.0 spec §11). Both
|
|
2
|
+
// the agent-instructions check (strand 1) and the gitignore check
|
|
3
|
+
// (strand 4) call into this module with their respective (marker-pair,
|
|
4
|
+
// canonical-text) tuples. Doctor's per-check code stays small; every
|
|
5
|
+
// managed-block check shares one implementation of the detect / extract
|
|
6
|
+
// / splice / hash primitives.
|
|
7
|
+
//
|
|
8
|
+
// Design rules from the spec:
|
|
9
|
+
// - Detection semantics enumerated at §2.6 / §4.4 (missing, stale-hash,
|
|
10
|
+
// legacy-markers, orphan-marker, duplicate-block).
|
|
11
|
+
// - --fix is wholesale replacement inside the markers; zero bytes
|
|
12
|
+
// touched outside; newline normalisation is off (§2.7).
|
|
13
|
+
// - Structurally corrupt files (orphan / duplicate) are refused by
|
|
14
|
+
// --fix; hand-repair message returned.
|
|
15
|
+
// - Idempotent: running --fix on already-clean state is a no-op that
|
|
16
|
+
// writes zero files (§2.7 last bullet).
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {'clean' | 'missing-block' | 'stale-hash' | 'legacy-markers' | 'orphan-marker' | 'duplicate-block'} BlockState
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} BlockOptions
|
|
26
|
+
* @property {string} markerBegin - current-generation begin marker.
|
|
27
|
+
* @property {string} markerEnd - current-generation end marker.
|
|
28
|
+
* @property {string | null} [legacyMarkerBegin] - pre-0.6.0 begin marker; omit to disable legacy detection.
|
|
29
|
+
* @property {string | null} [legacyMarkerEnd] - pre-0.6.0 end marker; omit to disable legacy detection.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* SHA-256 of the trimmed text, matching gen-managed-artefacts.mjs's
|
|
34
|
+
* hashOf. Whitespace around marker lines does not trip staleness.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} text
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function hashInnerContent(text) {
|
|
40
|
+
return createHash('sha256').update(text.trim(), 'utf8').digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Locate the marker pair's byte offsets within `text`. Returns
|
|
45
|
+
* `beginIndex` (offset of MARKER_BEGIN's first char), `endIndex`
|
|
46
|
+
* (offset of the character AFTER MARKER_END, INCLUSIVE of a trailing
|
|
47
|
+
* newline if one exists), and `innerText` (bytes strictly between the
|
|
48
|
+
* markers, no surrounding newlines).
|
|
49
|
+
*
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {string} markerBegin
|
|
52
|
+
* @param {string} markerEnd
|
|
53
|
+
* @returns {{ beginIndex: number, endIndex: number, innerText: string } | null}
|
|
54
|
+
*/
|
|
55
|
+
export function locateMarkers(text, markerBegin, markerEnd) {
|
|
56
|
+
const beginIndex = text.indexOf(markerBegin);
|
|
57
|
+
if (beginIndex < 0) return null;
|
|
58
|
+
const innerStart = beginIndex + markerBegin.length;
|
|
59
|
+
const endStart = text.indexOf(markerEnd, innerStart);
|
|
60
|
+
if (endStart < 0) return null;
|
|
61
|
+
let endIndex = endStart + markerEnd.length;
|
|
62
|
+
if (text[endIndex] === '\n') endIndex += 1;
|
|
63
|
+
const innerText = text.slice(innerStart, endStart);
|
|
64
|
+
return { beginIndex, endIndex, innerText };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Count occurrences of `needle` in `haystack`.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} haystack
|
|
71
|
+
* @param {string} needle
|
|
72
|
+
* @returns {number}
|
|
73
|
+
*/
|
|
74
|
+
function countOccurrences(haystack, needle) {
|
|
75
|
+
if (needle.length === 0) return 0;
|
|
76
|
+
let count = 0;
|
|
77
|
+
let i = 0;
|
|
78
|
+
while (true) {
|
|
79
|
+
const at = haystack.indexOf(needle, i);
|
|
80
|
+
if (at < 0) return count;
|
|
81
|
+
count += 1;
|
|
82
|
+
i = at + needle.length;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Classify the file's managed-block state given the current-generation
|
|
88
|
+
* markers, the pre-0.6.0 legacy markers (optional), and the canonical
|
|
89
|
+
* hash the caller expects the block's inner content to match.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} fileText - the file's raw bytes as UTF-8 string.
|
|
92
|
+
* @param {string} canonicalHash - SHA-256 of the current canonical text (trimmed).
|
|
93
|
+
* @param {BlockOptions} opts
|
|
94
|
+
* @returns {BlockState}
|
|
95
|
+
*/
|
|
96
|
+
export function classifyBlock(fileText, canonicalHash, opts) {
|
|
97
|
+
const { markerBegin, markerEnd, legacyMarkerBegin, legacyMarkerEnd } = opts;
|
|
98
|
+
const beginCount = countOccurrences(fileText, markerBegin);
|
|
99
|
+
const endCount = countOccurrences(fileText, markerEnd);
|
|
100
|
+
// Duplicate block detection first: more than one pair is unambiguous.
|
|
101
|
+
if (beginCount >= 2 && endCount >= 2) return 'duplicate-block';
|
|
102
|
+
// Orphan (unpaired current-marker) after duplicate; either half missing.
|
|
103
|
+
if (beginCount !== endCount) return 'orphan-marker';
|
|
104
|
+
// If neither current marker present, fall through to legacy / missing.
|
|
105
|
+
if (beginCount === 0) {
|
|
106
|
+
if (legacyMarkerBegin && legacyMarkerEnd) {
|
|
107
|
+
const legBegin = countOccurrences(fileText, legacyMarkerBegin);
|
|
108
|
+
const legEnd = countOccurrences(fileText, legacyMarkerEnd);
|
|
109
|
+
if (legBegin >= 1 && legEnd >= 1) return 'legacy-markers';
|
|
110
|
+
}
|
|
111
|
+
return 'missing-block';
|
|
112
|
+
}
|
|
113
|
+
// Exactly one pair present; hash the inner content.
|
|
114
|
+
const located = locateMarkers(fileText, markerBegin, markerEnd);
|
|
115
|
+
if (!located) return 'orphan-marker';
|
|
116
|
+
const innerHash = hashInnerContent(located.innerText);
|
|
117
|
+
return innerHash === canonicalHash ? 'clean' : 'stale-hash';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Produce the composed block string (markers + canonical text). Callers
|
|
122
|
+
* splice this into the file at the location `locateMarkers` reported.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} canonicalText
|
|
125
|
+
* @param {string} markerBegin
|
|
126
|
+
* @param {string} markerEnd
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
export function composeBlock(canonicalText, markerBegin, markerEnd) {
|
|
130
|
+
const trimmed = canonicalText.trim();
|
|
131
|
+
return `${markerBegin}\n${trimmed}\n${markerEnd}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Rewrite `fileText` in place with the composed block:
|
|
136
|
+
* - `clean`: return the input unchanged (idempotent no-op).
|
|
137
|
+
* - `stale-hash`: splice the composed block over the current pair,
|
|
138
|
+
* preserving every byte outside the markers.
|
|
139
|
+
* - `legacy-markers`: splice the composed block over the legacy pair,
|
|
140
|
+
* preserving every byte outside the legacy markers.
|
|
141
|
+
* - `missing-block`: append the composed block at end of file, adding a
|
|
142
|
+
* leading blank-line separator if the file did not end in a newline.
|
|
143
|
+
* - `orphan-marker` / `duplicate-block`: refused; returns null so the
|
|
144
|
+
* caller emits the hand-repair message.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} fileText
|
|
147
|
+
* @param {string} canonicalText
|
|
148
|
+
* @param {BlockOptions} opts
|
|
149
|
+
* @param {string} canonicalHash
|
|
150
|
+
* @returns {{ nextText: string, action: 'noop' | 'replaced' | 'migrated' | 'appended' } | null}
|
|
151
|
+
*/
|
|
152
|
+
export function applyFix(fileText, canonicalText, opts, canonicalHash) {
|
|
153
|
+
const state = classifyBlock(fileText, canonicalHash, opts);
|
|
154
|
+
const composed = composeBlock(canonicalText, opts.markerBegin, opts.markerEnd);
|
|
155
|
+
if (state === 'clean') {
|
|
156
|
+
return { nextText: fileText, action: 'noop' };
|
|
157
|
+
}
|
|
158
|
+
if (state === 'orphan-marker' || state === 'duplicate-block') {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (state === 'stale-hash') {
|
|
162
|
+
const loc = locateMarkers(fileText, opts.markerBegin, opts.markerEnd);
|
|
163
|
+
if (!loc) return null;
|
|
164
|
+
const nextText = fileText.slice(0, loc.beginIndex) + composed + fileText.slice(loc.endIndex);
|
|
165
|
+
return { nextText, action: 'replaced' };
|
|
166
|
+
}
|
|
167
|
+
if (state === 'legacy-markers' && opts.legacyMarkerBegin && opts.legacyMarkerEnd) {
|
|
168
|
+
const loc = locateMarkers(fileText, opts.legacyMarkerBegin, opts.legacyMarkerEnd);
|
|
169
|
+
if (!loc) return null;
|
|
170
|
+
const nextText = fileText.slice(0, loc.beginIndex) + composed + fileText.slice(loc.endIndex);
|
|
171
|
+
return { nextText, action: 'migrated' };
|
|
172
|
+
}
|
|
173
|
+
// missing-block
|
|
174
|
+
const separator = fileText.length === 0
|
|
175
|
+
? ''
|
|
176
|
+
: (fileText.endsWith('\n') ? '\n' : '\n\n');
|
|
177
|
+
return { nextText: `${fileText}${separator}${composed}`, action: 'appended' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract only the inner content of the current-marker block, or null
|
|
182
|
+
* if the file has no clean pair. For diagnostics: doctor's `stale-hash`
|
|
183
|
+
* report can quote or diff the block if it wants to (v1 does not).
|
|
184
|
+
*
|
|
185
|
+
* @param {string} fileText
|
|
186
|
+
* @param {string} markerBegin
|
|
187
|
+
* @param {string} markerEnd
|
|
188
|
+
* @returns {string | null}
|
|
189
|
+
*/
|
|
190
|
+
export function extractInnerContent(fileText, markerBegin, markerEnd) {
|
|
191
|
+
const located = locateMarkers(fileText, markerBegin, markerEnd);
|
|
192
|
+
return located ? located.innerText : null;
|
|
193
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Aggregator seam for the managed `.gitignore` block (0.6.0 spec §4,
|
|
2
|
+
// D-4 normative aggregator function). The registry of "files RCF wants
|
|
3
|
+
// ignored by default" is composed by `managedGitignoreEntries()`. This
|
|
4
|
+
// module is the single shared file that 0.7.0+ extends: import a
|
|
5
|
+
// per-feature entry constant from its owning module and insert into the
|
|
6
|
+
// array, one file, two lines added, no doctor code change (§4.1's
|
|
7
|
+
// worked 0.7.0 diff).
|
|
8
|
+
//
|
|
9
|
+
// The `.gitignore` marker convention shares the visual shape of the
|
|
10
|
+
// agent-instructions markers but lives in a disjoint namespace
|
|
11
|
+
// (gitignore uses `#` for comments). Doctor's per-check logic still
|
|
12
|
+
// calls into the generic managed-block primitive in `managed-block.js`
|
|
13
|
+
// with these markers as its (markerBegin, markerEnd) pair.
|
|
14
|
+
//
|
|
15
|
+
// Composition primitives split by concern:
|
|
16
|
+
// - Production callers use the no-arg accessors `composeGitignoreBlock()`,
|
|
17
|
+
// `composeGitignoreInner()`, `computeGitignoreBlockHash()` — these
|
|
18
|
+
// compose from the module-owned aggregator (`managedGitignoreEntries()`).
|
|
19
|
+
// - Tests that need to prove the extension pipeline works with a
|
|
20
|
+
// synthetic second entry call the `*FromEntries(entries)` helpers
|
|
21
|
+
// directly with an explicit entries array. No production code path
|
|
22
|
+
// accepts an entries override; the aggregator's registered set is the
|
|
23
|
+
// only source of truth at runtime.
|
|
24
|
+
|
|
25
|
+
import { hashInnerContent } from './managed-block.js';
|
|
26
|
+
import { identityEntry } from './identity-seed.js';
|
|
27
|
+
import { preflightEntry } from '../preflight/secrets.js';
|
|
28
|
+
// Track C+D §9.3: gitignore the view-server pid file and supervisor log.
|
|
29
|
+
import { viewServerGitignoreEntry, viewServerLogGitignoreEntry } from '../view-supervisor/manifest-writer.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} GitignoreEntry
|
|
33
|
+
* @property {string} path - gitignore-syntax line (glob or literal path relative to project root).
|
|
34
|
+
* @property {string} owner - human-readable feature name shown in the block's owner comment.
|
|
35
|
+
* @property {string} since - SemVer minor the entry landed on.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registered gitignore entries the managed block covers. Each entry
|
|
40
|
+
* exports from its owning feature module (per §4.1's contract: feature
|
|
41
|
+
* modules OWN their entry constant and do NOT import the aggregator,
|
|
42
|
+
* avoiding import-time mutation surprises); the aggregator imports and
|
|
43
|
+
* composes deterministically. Adding an entry (e.g. the 0.7.0
|
|
44
|
+
* credentials side-file) is a one-file edit here: import the constant
|
|
45
|
+
* and insert into the returned array. Do NOT mutate the returned array
|
|
46
|
+
* at runtime; callers should treat it as read-only.
|
|
47
|
+
*
|
|
48
|
+
* @returns {GitignoreEntry[]}
|
|
49
|
+
*/
|
|
50
|
+
export function managedGitignoreEntries() {
|
|
51
|
+
return [
|
|
52
|
+
identityEntry,
|
|
53
|
+
preflightEntry,
|
|
54
|
+
viewServerGitignoreEntry,
|
|
55
|
+
viewServerLogGitignoreEntry,
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Owner-comment header on the begin marker (§4.3 canonical text). */
|
|
60
|
+
export const GITIGNORE_MARKER_BEGIN = '# rcf:managed:begin (managed by `rcf doctor`; do not edit inside)';
|
|
61
|
+
export const GITIGNORE_MARKER_END = '# rcf:managed:end';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compose the full managed block (marker + inner + marker + terminating
|
|
65
|
+
* newline) from an explicit entries array. Pure helper — used by the
|
|
66
|
+
* production accessor `composeGitignoreBlock()` (which sources entries
|
|
67
|
+
* from the aggregator) AND by tests that need to exercise the
|
|
68
|
+
* composition pipeline with a synthetic entries array without any
|
|
69
|
+
* production test hook.
|
|
70
|
+
*
|
|
71
|
+
* @param {GitignoreEntry[]} entries
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function composeGitignoreBlockFromEntries(entries) {
|
|
75
|
+
const lines = [GITIGNORE_MARKER_BEGIN];
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
lines.push(`# ${e.owner} (since ${e.since})`);
|
|
78
|
+
lines.push(e.path);
|
|
79
|
+
}
|
|
80
|
+
lines.push(GITIGNORE_MARKER_END);
|
|
81
|
+
return `${lines.join('\n')}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Compose the inner content the block wraps (everything strictly
|
|
86
|
+
* between the marker lines) from an explicit entries array. Doctor's
|
|
87
|
+
* stale-hash primitive hashes this (trimmed) so operator whitespace
|
|
88
|
+
* around markers is ignored.
|
|
89
|
+
*
|
|
90
|
+
* @param {GitignoreEntry[]} entries
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function composeGitignoreInnerFromEntries(entries) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
lines.push(`# ${e.owner} (since ${e.since})`);
|
|
97
|
+
lines.push(e.path);
|
|
98
|
+
}
|
|
99
|
+
return `${lines.join('\n')}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* SHA-256 of the composed inner content (trimmed) for an explicit
|
|
104
|
+
* entries array. Doctor compares this to the hash of the inner content
|
|
105
|
+
* extracted from the file; mismatch means stale.
|
|
106
|
+
*
|
|
107
|
+
* @param {GitignoreEntry[]} entries
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
export function computeGitignoreBlockHashFromEntries(entries) {
|
|
111
|
+
return hashInnerContent(composeGitignoreInnerFromEntries(entries));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compose the full managed block from the aggregator. Deterministic
|
|
116
|
+
* order (aggregator's registered order); each entry preceded by its
|
|
117
|
+
* one-line owner comment `# {owner} (since {since})`; trailing newline
|
|
118
|
+
* outside the end marker so the block sits cleanly inside a file.
|
|
119
|
+
* Callers splice the return value in place; the return is the FULL
|
|
120
|
+
* block (marker + inner + marker + terminating newline).
|
|
121
|
+
*
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
export function composeGitignoreBlock() {
|
|
125
|
+
return composeGitignoreBlockFromEntries(managedGitignoreEntries());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The inner-content string the composed block wraps, sourced from the
|
|
130
|
+
* aggregator.
|
|
131
|
+
*
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function composeGitignoreInner() {
|
|
135
|
+
return composeGitignoreInnerFromEntries(managedGitignoreEntries());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* SHA-256 of the composed inner content (trimmed), sourced from the
|
|
140
|
+
* aggregator. Doctor's `stale-hash` production callsite.
|
|
141
|
+
*
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
export function computeGitignoreBlockHash() {
|
|
145
|
+
return computeGitignoreBlockHashFromEntries(managedGitignoreEntries());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Locate the managed gitignore block within `text`. Same primitive as
|
|
150
|
+
* managed-block.js's `locateMarkers` but exported here for the doctor
|
|
151
|
+
* flow's convenience.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} text
|
|
154
|
+
* @returns {{ beginIndex: number, endIndex: number, innerText: string } | null}
|
|
155
|
+
*/
|
|
156
|
+
export function extractGitignoreBlock(text) {
|
|
157
|
+
const beginIndex = text.indexOf(GITIGNORE_MARKER_BEGIN);
|
|
158
|
+
if (beginIndex < 0) return null;
|
|
159
|
+
const innerStart = beginIndex + GITIGNORE_MARKER_BEGIN.length;
|
|
160
|
+
const endStart = text.indexOf(GITIGNORE_MARKER_END, innerStart);
|
|
161
|
+
if (endStart < 0) return null;
|
|
162
|
+
let endIndex = endStart + GITIGNORE_MARKER_END.length;
|
|
163
|
+
if (text[endIndex] === '\n') endIndex += 1;
|
|
164
|
+
const innerText = text.slice(innerStart, endStart);
|
|
165
|
+
return { beginIndex, endIndex, innerText };
|
|
166
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Marker constants for the agent-instructions managed block (0.6.0
|
|
2
|
+
// spec §2.5, D-7). Extracted to a single module so a marker rename in
|
|
3
|
+
// six months lands in one place, not scattered across agent-setup,
|
|
4
|
+
// doctor, MCP funnel and tests. Drift between two definitions is the
|
|
5
|
+
// exact bug the strand-1 legacy-markers migration exists to prevent.
|
|
6
|
+
//
|
|
7
|
+
// The legacy pair (`<!-- rcf:begin -->` / `<!-- rcf:end -->`) is also
|
|
8
|
+
// exported so callers that need to recognise the pre-0.6.0 marker
|
|
9
|
+
// generation (the MCP setup funnel, doctor's `legacy-markers` check)
|
|
10
|
+
// have a single source of truth for its shape too.
|
|
11
|
+
//
|
|
12
|
+
// The managed-gitignore module owns its own marker constants in
|
|
13
|
+
// `managed-gitignore.js` (disjoint namespace: gitignore uses `#`
|
|
14
|
+
// comments rather than HTML comments).
|
|
15
|
+
|
|
16
|
+
/** The 0.6.0+ managed-block markers `rcf init` writes into agent-instructions files. */
|
|
17
|
+
export const MARKER_BEGIN = '<!-- rcf:managed:begin -->';
|
|
18
|
+
export const MARKER_END = '<!-- rcf:managed:end -->';
|
|
19
|
+
|
|
20
|
+
/** Pre-0.6.0 marker generation; kept for the transitional `hasAgentMarker` recogniser and doctor's `legacy-markers` check. */
|
|
21
|
+
export const LEGACY_MARKER_BEGIN = '<!-- rcf:begin -->';
|
|
22
|
+
export const LEGACY_MARKER_END = '<!-- rcf:end -->';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compose the replace-in-place regex for the current-marker managed
|
|
26
|
+
* block. Non-greedy match between MARKER_BEGIN and MARKER_END so a file
|
|
27
|
+
* with structural corruption (unpaired markers, duplicate blocks) does
|
|
28
|
+
* not silently swallow arbitrary regions - doctor detects those states
|
|
29
|
+
* explicitly and refuses to --fix them.
|
|
30
|
+
*
|
|
31
|
+
* @returns {RegExp}
|
|
32
|
+
*/
|
|
33
|
+
export function markerRegex() {
|
|
34
|
+
return new RegExp(`${escapeRegex(MARKER_BEGIN)}[\\s\\S]*?${escapeRegex(MARKER_END)}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Same shape as `markerRegex` for the legacy pair. Used by doctor's
|
|
39
|
+
* `legacy-markers` --fix path and by the transitional migration.
|
|
40
|
+
*
|
|
41
|
+
* @returns {RegExp}
|
|
42
|
+
*/
|
|
43
|
+
export function legacyMarkerRegex() {
|
|
44
|
+
return new RegExp(`${escapeRegex(LEGACY_MARKER_BEGIN)}[\\s\\S]*?${escapeRegex(LEGACY_MARKER_END)}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeRegex(s) {
|
|
48
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
49
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Ruled UI-baseline defaults (ui-design-gate-0.7.0-spec §6.1).
|
|
2
|
+
//
|
|
3
|
+
// Every value is Baz-ruled (Entry 5 of the 2026-07-29 cold-run operator
|
|
4
|
+
// feedback log) or a §4.3 mandate from the review-phase analysis. The
|
|
5
|
+
// `rcf ui-baseline init` verb presents these values as the defaults;
|
|
6
|
+
// the operator accepts (silence is NOT an opt-out) or overrides with a
|
|
7
|
+
// reason via `operatorOptOuts[]` (spec §6.2).
|
|
8
|
+
//
|
|
9
|
+
// Editing this catalogue is a spec-level change: baseline evolution
|
|
10
|
+
// happens via `rcf ui-baseline opt-out` per project, or a new spec
|
|
11
|
+
// revision.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} UiBaselineFieldSpec
|
|
15
|
+
* @property {string} path dot-path into `uiBaseline.defaults` (for opt-out records + refusal messages)
|
|
16
|
+
* @property {*} value the ruled value (the "present as default" for interactive init)
|
|
17
|
+
* @property {string} label operator-facing short label (used in the summary screen)
|
|
18
|
+
* @property {string} rulingSource brief citation for provenance
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The v1 defaults catalogue. Ordered for stable presentation on the
|
|
23
|
+
* interactive summary screen: theme + layout first (most visible),
|
|
24
|
+
* then contrast + a11y, then typography + interaction, then auth flow.
|
|
25
|
+
* @type {UiBaselineFieldSpec[]}
|
|
26
|
+
*/
|
|
27
|
+
export const UI_BASELINE_DEFAULTS_V1 = Object.freeze([
|
|
28
|
+
{ path: 'themeMode', value: 'light-default-with-toggle', label: 'Theme mode', rulingSource: 'Baz Entry 5' },
|
|
29
|
+
{ path: 'sharedLayoutModule', value: 'src/ui/layout.ts', label: 'Shared layout module', rulingSource: 'Baz Entry 5 + review-phase mandate 3' },
|
|
30
|
+
{ path: 'designTokensModule', value: 'src/ui/tokens.ts', label: 'Design tokens module', rulingSource: 'review-phase mandate 1' },
|
|
31
|
+
{ path: 'noHexInViewFiles', value: true, label: 'No hex literals in view files', rulingSource: 'review-phase mandate 1' },
|
|
32
|
+
{ path: 'contrastTarget', value: 'WCAG AA', label: 'Contrast target', rulingSource: 'Baz Entry 5 + review-phase mandate 6' },
|
|
33
|
+
{ path: 'contrastTestBeforePalette', value: true, label: 'Contrast test authored before palette', rulingSource: 'review-phase mandate 10' },
|
|
34
|
+
{ path: 'focusRingsRequired', value: true, label: 'Focus rings required', rulingSource: 'review-phase mandate 6' },
|
|
35
|
+
{ path: 'hoverStatesRequired', value: true, label: 'Hover states required', rulingSource: 'review-phase mandate 8' },
|
|
36
|
+
{ path: 'componentVocabulary.declaredComponents', value: ['Button', 'Input', 'Card', 'Badge', 'Table', 'Notice'], label: 'Component vocabulary', rulingSource: 'review-phase mandate 5' },
|
|
37
|
+
{ path: 'componentVocabulary.singleBadgeShape', value: true, label: 'Single badge shape', rulingSource: 'review-phase mandate 5' },
|
|
38
|
+
{ path: 'typography.baseFontStack', value: 'system-ui', label: 'Base font stack', rulingSource: 'review-phase mandate 7' },
|
|
39
|
+
{ path: 'typography.bodyLineHeight', value: 1.5, label: 'Body line height', rulingSource: 'review-phase mandate 7' },
|
|
40
|
+
{ path: 'typography.headingLineHeight', value: 1.25, label: 'Heading line height', rulingSource: 'review-phase mandate 7' },
|
|
41
|
+
{ path: 'typography.proseMaxWidth', value: '72ch', label: 'Prose max width', rulingSource: 'review-phase mandate 7' },
|
|
42
|
+
{ path: 'interactionDefaults.loadingIndicatorOnFetch', value: true, label: 'Loading indicator on fetch', rulingSource: 'review-phase mandate 8' },
|
|
43
|
+
{ path: 'interactionDefaults.disabledStateVisuallyDistinct', value: true, label: 'Disabled state visually distinct', rulingSource: 'review-phase mandate 8' },
|
|
44
|
+
{ path: 'authFlow.htmlLoginPageRequired', value: true, label: 'HTML login page required', rulingSource: 'review-phase mandate 9 + Baz "real login flow"' },
|
|
45
|
+
{ path: 'authFlow.smokeChecksRequired', value: true, label: 'Auth-REQ smoke checks required', rulingSource: 'review-phase recommendation 9' },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deep-set a value at a dot-path on a plain object. Intermediate
|
|
50
|
+
* objects are created as needed. Arrays are copied (not shared with
|
|
51
|
+
* the caller). Returns the mutated top-level object for chaining.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} obj
|
|
54
|
+
* @param {string} path dot-path (e.g. `componentVocabulary.declaredComponents`)
|
|
55
|
+
* @param {*} value
|
|
56
|
+
* @returns {object}
|
|
57
|
+
*/
|
|
58
|
+
export function deepSet(obj, path, value) {
|
|
59
|
+
const parts = String(path).split('.');
|
|
60
|
+
let cursor = obj;
|
|
61
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
62
|
+
const key = parts[i];
|
|
63
|
+
if (typeof cursor[key] !== 'object' || cursor[key] === null || Array.isArray(cursor[key])) {
|
|
64
|
+
cursor[key] = {};
|
|
65
|
+
}
|
|
66
|
+
cursor = cursor[key];
|
|
67
|
+
}
|
|
68
|
+
const last = parts[parts.length - 1];
|
|
69
|
+
cursor[last] = Array.isArray(value) ? [...value] : value;
|
|
70
|
+
return obj;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deep-get a value at a dot-path. Returns `undefined` when any
|
|
75
|
+
* intermediate segment is absent.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} obj
|
|
78
|
+
* @param {string} path
|
|
79
|
+
* @returns {*}
|
|
80
|
+
*/
|
|
81
|
+
export function deepGet(obj, path) {
|
|
82
|
+
const parts = String(path).split('.');
|
|
83
|
+
let cursor = obj;
|
|
84
|
+
for (const key of parts) {
|
|
85
|
+
if (cursor === null || cursor === undefined) return undefined;
|
|
86
|
+
cursor = cursor[key];
|
|
87
|
+
}
|
|
88
|
+
return cursor;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compose the ruled defaults object from the v1 catalogue. Every field
|
|
93
|
+
* carries its Baz-ruled value; caller overrides land under
|
|
94
|
+
* `operatorOptOuts[]` on the surrounding record.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} [overrides] dot-path -> value overrides (used by preflight seam pickup)
|
|
97
|
+
* @returns {object}
|
|
98
|
+
*/
|
|
99
|
+
export function composeDefaults(overrides = {}) {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const spec of UI_BASELINE_DEFAULTS_V1) {
|
|
102
|
+
deepSet(out, spec.path, spec.value);
|
|
103
|
+
}
|
|
104
|
+
for (const [path, value] of Object.entries(overrides ?? {})) {
|
|
105
|
+
deepSet(out, path, value);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* True when the given dot-path is a known baseline field. Used by
|
|
112
|
+
* `rcf ui-baseline opt-out` to refuse writes to non-existent fields.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} path
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function isKnownBaselinePath(path) {
|
|
118
|
+
return UI_BASELINE_DEFAULTS_V1.some((s) => s.path === path);
|
|
119
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Public surface for the UI-baseline module (ui-design-gate-0.7.0
|
|
2
|
+
// spec §5.4, §6). The CLI handler lives at `src/cli/ui-baseline.js`;
|
|
3
|
+
// consumers reach the module through this barrel.
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
UI_BASELINE_DEFAULTS_V1,
|
|
7
|
+
composeDefaults,
|
|
8
|
+
deepGet,
|
|
9
|
+
deepSet,
|
|
10
|
+
isKnownBaselinePath,
|
|
11
|
+
} from './defaults.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
baselineDesignDisagreement,
|
|
15
|
+
composeUiBaselineRecord,
|
|
16
|
+
nextUiBaselineId,
|
|
17
|
+
preflightSeamOverrides,
|
|
18
|
+
writeUiBaselineOptOut,
|
|
19
|
+
writeUiBaselineRecord,
|
|
20
|
+
} from './manifest-writer.js';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
normaliseNonInteractiveInput,
|
|
24
|
+
runInteractiveSession,
|
|
25
|
+
} from './session.js';
|