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/mcp.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// `rcf mcp` subcommand handler (Phase 7 §D1 / §D13). Resolves the
|
|
2
|
+
// project root ONCE at startup, wires process stdin / stdout / stderr
|
|
3
|
+
// into the MCP server core, and serves until stdin EOF or a signal.
|
|
4
|
+
//
|
|
5
|
+
// Stream discipline (D2): stdout carries MCP messages and NOTHING
|
|
6
|
+
// else. All logging goes to stderr; default stderr output is one
|
|
7
|
+
// startup line plus unexpected-failure blocks, with per-request lines
|
|
8
|
+
// behind --verbose. No root found: one stderr line and exit 2 BEFORE
|
|
9
|
+
// any stdout traffic - a misconfigured client sees a dead subprocess
|
|
10
|
+
// and a clear stderr message.
|
|
11
|
+
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
14
|
+
import { parseArgs } from 'node:util';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
import { findProjectRoot } from '../view/index.js';
|
|
18
|
+
import { createMcpServer, serveStreams } from '#core/mcp-shell';
|
|
19
|
+
import { createToolRegistry } from '../mcp/tools.js';
|
|
20
|
+
import { createResourceRegistry } from '../mcp/resources.js';
|
|
21
|
+
import { createPromptRegistry } from '../mcp/prompts.js';
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
|
|
25
|
+
const OPTION_SPEC = {
|
|
26
|
+
'project-root': { type: 'string' },
|
|
27
|
+
verbose: { type: 'boolean' },
|
|
28
|
+
help: { type: 'boolean' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const HELP = `Usage: rcf mcp [options]
|
|
32
|
+
|
|
33
|
+
Serve the project over the Model Context Protocol (local stdio). An
|
|
34
|
+
MCP-capable agent harness launches 'rcf mcp' as a subprocess in a
|
|
35
|
+
project directory; stdout carries protocol messages only, logging
|
|
36
|
+
goes to stderr.
|
|
37
|
+
|
|
38
|
+
Serves eleven tools (rcf_validate, rcf_coverage, rcf_trace,
|
|
39
|
+
rcf_impact, rcf_read, rcf_create, rcf_update, rcf_delete, rcf_link,
|
|
40
|
+
rcf_unlink, rcf_build), resources (rcf://tree, rcf://doc/<id>,
|
|
41
|
+
rcf://docs/<slug>) and the agent-guidance prompts.
|
|
42
|
+
|
|
43
|
+
The project root is resolved once at startup and fixed for the
|
|
44
|
+
process lifetime; multi-project harnesses run multiple server
|
|
45
|
+
entries. The server exits when the client closes stdin.
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--project-root <path> Resolve the project root from <path>
|
|
49
|
+
instead of the working directory
|
|
50
|
+
--verbose Per-request logging on stderr
|
|
51
|
+
--help Print this help
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const INSTRUCTIONS = 'Filesystem-backed RCF (Requirements Confidence Framework) project. '
|
|
55
|
+
+ 'Start with rcf_validate to check tree health, and the rcf://tree resource to orient. '
|
|
56
|
+
+ 'Coverage is structural, not semantic: rcf_coverage reports whether chains reach test '
|
|
57
|
+
+ 'cases, not whether acceptance criteria adequately capture intent. Write tools edit '
|
|
58
|
+
+ 'the git-tracked rcf/ tree directly.';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string[]} argv - argv slice after `mcp`
|
|
62
|
+
* @param {object} [deps]
|
|
63
|
+
* @returns {Promise<number>}
|
|
64
|
+
*/
|
|
65
|
+
export async function main(argv, deps = {}) {
|
|
66
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
67
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
68
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
69
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
70
|
+
const onSignal = deps.onSignal ?? ((sig, fn) => process.on(sig, fn));
|
|
71
|
+
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
77
|
+
stderr.write(HELP);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
const flags = parsed.values;
|
|
81
|
+
if (flags.help) {
|
|
82
|
+
stdout.write(HELP);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (parsed.positionals.length > 0) {
|
|
86
|
+
stderr.write('[error] usage mcp: takes no positional arguments\n');
|
|
87
|
+
stderr.write(HELP);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// D13: resolve the root once, before any protocol traffic.
|
|
92
|
+
const start = flags['project-root'] ? resolvePath(cwd, flags['project-root']) : cwd;
|
|
93
|
+
const projectRoot = await findProjectRoot(start);
|
|
94
|
+
if (!projectRoot) {
|
|
95
|
+
// B3 (E2E matrix 2026-07-06-003): an MCP client surfaces this failure
|
|
96
|
+
// as "zero tools" with no visible error, so the stderr line must tell
|
|
97
|
+
// the operator exactly what to do next. Theme 1 funnel: incomplete
|
|
98
|
+
// setup always routes to `npx rcf init` + a session restart.
|
|
99
|
+
stderr.write(`[error] usage no project root found (no rcf/manifest.json in ${start} or any ancestor). `
|
|
100
|
+
+ 'The MCP server needs an existing rcf/ tree - run `npx rcf init` in the project first '
|
|
101
|
+
+ '(it wires the tree, .mcp.json and the agent instructions), then restart your agent '
|
|
102
|
+
+ 'session; or pass --project-root <path>. See docs/install.md, section 7.\n');
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const verbose = Boolean(flags.verbose);
|
|
107
|
+
const log = {
|
|
108
|
+
info: verbose ? (line) => stderr.write(`[rcf mcp] ${line}\n`) : () => {},
|
|
109
|
+
error: (line) => stderr.write(`${line}\n`),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const tools = createToolRegistry({ projectRoot, log });
|
|
113
|
+
const resources = createResourceRegistry({ projectRoot });
|
|
114
|
+
const prompts = createPromptRegistry();
|
|
115
|
+
|
|
116
|
+
const server = createMcpServer({
|
|
117
|
+
serverInfo: { name: 'rcf-build-lite', version: await readPackageVersion() },
|
|
118
|
+
instructions: INSTRUCTIONS,
|
|
119
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
120
|
+
handlers: {
|
|
121
|
+
'tools/list': async () => ({ tools: tools.definitions }),
|
|
122
|
+
'tools/call': async (params) => {
|
|
123
|
+
log.info(`tools/call ${params.name}`);
|
|
124
|
+
return await tools.call(params.name, params.arguments);
|
|
125
|
+
},
|
|
126
|
+
'resources/list': async () => await resources.list(),
|
|
127
|
+
'resources/read': async (params) => {
|
|
128
|
+
log.info(`resources/read ${params.uri}`);
|
|
129
|
+
return await resources.read(params.uri);
|
|
130
|
+
},
|
|
131
|
+
'prompts/list': async () => await prompts.list(),
|
|
132
|
+
'prompts/get': async (params) => {
|
|
133
|
+
log.info(`prompts/get ${params?.name}`);
|
|
134
|
+
return await prompts.get(params);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
log,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
stderr.write(`rcf mcp: serving ${projectRoot}\n`);
|
|
141
|
+
|
|
142
|
+
const { done, stop } = serveStreams(server, { input: stdin, output: stdout, log });
|
|
143
|
+
|
|
144
|
+
// D18: SIGINT / SIGTERM exit cleanly - nothing to drain, no ports,
|
|
145
|
+
// no watchers. EOF on stdin is the spec's termination path.
|
|
146
|
+
onSignal('SIGINT', () => { log.info('received SIGINT, shutting down'); stop(); });
|
|
147
|
+
onSignal('SIGTERM', () => { log.info('received SIGTERM, shutting down'); stop(); });
|
|
148
|
+
|
|
149
|
+
return await done;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readPackageVersion() {
|
|
153
|
+
try {
|
|
154
|
+
const pkgPath = resolvePath(here, '..', '..', 'package.json');
|
|
155
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
156
|
+
return pkg.version ?? '0.0.0';
|
|
157
|
+
} catch {
|
|
158
|
+
return '0.0.0';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// `rcf preflight` subcommand handler
|
|
2
|
+
// (verification-integrity-cluster-spec §4, ADDENDUM §A).
|
|
3
|
+
//
|
|
4
|
+
// Runs the pre-flight config elicitation session and writes a
|
|
5
|
+
// `preFlightConfig` record onto the manifest. Interactive mode by
|
|
6
|
+
// default when stdin + stdout are TTYs; non-interactive mode reads a
|
|
7
|
+
// pre-filled JSON file (--input) for CI / automation. The session
|
|
8
|
+
// composes:
|
|
9
|
+
// - one per-service ruling per candidate the scanner surfaced (§4.5),
|
|
10
|
+
// plus operator additions;
|
|
11
|
+
// - one design-shape answer per applicable REQ + catalogue question
|
|
12
|
+
// (ADDENDUM §A.1);
|
|
13
|
+
// - one baselineAcOptOuts entry per design-shape answer that triggers
|
|
14
|
+
// an opt-out (ADDENDUM §A.2).
|
|
15
|
+
//
|
|
16
|
+
// Credentials never enter the chain: the session records only env-var
|
|
17
|
+
// NAMES + presence booleans, via the gitignored
|
|
18
|
+
// `.rcf/preflight-secrets.local.json` side-file whose gitignore path
|
|
19
|
+
// rides the 0.6.0 aggregator seam (§4.6).
|
|
20
|
+
|
|
21
|
+
import { readFile } from 'node:fs/promises';
|
|
22
|
+
import { parseArgs } from 'node:util';
|
|
23
|
+
import { createInterface } from 'node:readline/promises';
|
|
24
|
+
import process from 'node:process';
|
|
25
|
+
|
|
26
|
+
import { walkTree } from '#core/store';
|
|
27
|
+
import { writeUnexpectedFailure, rcfError } from '#core/errors';
|
|
28
|
+
|
|
29
|
+
import { findProjectRoot } from '../view/index.js';
|
|
30
|
+
import {
|
|
31
|
+
composeDesignShapeOptOuts,
|
|
32
|
+
composePreflightRecord,
|
|
33
|
+
normaliseNonInteractiveInput,
|
|
34
|
+
runInteractiveSession,
|
|
35
|
+
scanForServiceCandidates,
|
|
36
|
+
writePreflightRecord,
|
|
37
|
+
} from '../preflight/index.js';
|
|
38
|
+
|
|
39
|
+
const OPTION_SPEC = {
|
|
40
|
+
prd: { type: 'string' },
|
|
41
|
+
'from-tad': { type: 'string' },
|
|
42
|
+
input: { type: 'string' },
|
|
43
|
+
'non-interactive': { type: 'boolean' },
|
|
44
|
+
'dry-run': { type: 'boolean' },
|
|
45
|
+
quiet: { type: 'boolean' },
|
|
46
|
+
json: { type: 'boolean' },
|
|
47
|
+
help: { type: 'boolean' },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const HELP = `Usage: rcf preflight [options]
|
|
51
|
+
|
|
52
|
+
Elicit the pre-flight configuration record: for every third-party
|
|
53
|
+
service the PRD or TAD names, force one of the five attestation modes,
|
|
54
|
+
and record the operator's ruling on the manifest. Also poses any
|
|
55
|
+
applicable design-shape questions (v1 catalogue: auth.htmlLoginPage).
|
|
56
|
+
|
|
57
|
+
Interactive by default when running on a TTY. Non-interactive mode
|
|
58
|
+
reads a JSON file matching the session-summary shape.
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--prd <prd-id> PRD to scan (default: the project's PRD)
|
|
62
|
+
--from-tad <tad-id> Also scan this TAD (default: the project's TAD)
|
|
63
|
+
--input <path> Non-interactive: read pre-filled session
|
|
64
|
+
--non-interactive Force non-interactive mode (default when
|
|
65
|
+
not on a TTY or when piped)
|
|
66
|
+
--dry-run Print the composed record; do not write
|
|
67
|
+
--json Emit the composed record as JSON to stdout
|
|
68
|
+
--quiet Suppress non-error stdout
|
|
69
|
+
--help Print this help
|
|
70
|
+
|
|
71
|
+
Credentials NEVER enter the chain. The session prompts for env var
|
|
72
|
+
NAMES only; values are read from the shell at test / finalise time.
|
|
73
|
+
The name-metadata is written to .rcf/preflight-secrets.local.json,
|
|
74
|
+
which is gitignored via the managed block written by rcf init.
|
|
75
|
+
|
|
76
|
+
Exit codes:
|
|
77
|
+
0 success
|
|
78
|
+
1 IO / unexpected runtime failure
|
|
79
|
+
2 usage error (bad flags, unresolvable PRD id)
|
|
80
|
+
3 schema validation on the composed record
|
|
81
|
+
4 operator cancelled the session at the confirm step
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string[]} argv
|
|
86
|
+
* @param {object} [deps]
|
|
87
|
+
* @returns {Promise<number>}
|
|
88
|
+
*/
|
|
89
|
+
export async function main(argv, deps = {}) {
|
|
90
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
91
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
92
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
93
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
94
|
+
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
100
|
+
stderr.write(HELP);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const flags = parsed.values;
|
|
104
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
105
|
+
if (parsed.positionals.length > 0) {
|
|
106
|
+
stderr.write('[error] usage preflight: no positional arguments (use --prd / --from-tad)\n');
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
111
|
+
if (!projectRoot) {
|
|
112
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` first.\n');
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const walkResult = await walkTree({ projectRoot });
|
|
117
|
+
if (walkResult.errors.length > 0) {
|
|
118
|
+
stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding — run 'rcf validate' for details\n`);
|
|
119
|
+
}
|
|
120
|
+
const { tree } = walkResult;
|
|
121
|
+
|
|
122
|
+
const prdId = flags.prd ?? tree.prd?.prdId;
|
|
123
|
+
if (!prdId) {
|
|
124
|
+
stderr.write('[error] usage preflight: no --prd given and no PRD found on the tree\n');
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
const tadId = flags['from-tad'] ?? tree.tad?.tadId ?? null;
|
|
128
|
+
|
|
129
|
+
const scan = scanForServiceCandidates({ tree, prdId, tadId });
|
|
130
|
+
if (scan.skippedDocIds.length > 0) {
|
|
131
|
+
stderr.write(`[warn] preflight: skipped ids not found on the tree: ${scan.skippedDocIds.join(', ')}\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isTty = Boolean(stdout.isTTY && stdin.isTTY);
|
|
135
|
+
const forceNonInteractive = Boolean(flags['non-interactive']) || Boolean(flags.input);
|
|
136
|
+
const interactive = !forceNonInteractive && isTty;
|
|
137
|
+
|
|
138
|
+
let sessionResult;
|
|
139
|
+
try {
|
|
140
|
+
if (interactive) {
|
|
141
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
142
|
+
const prompt = (q) => rl.question(q);
|
|
143
|
+
const write = (line) => stdout.write(`${line}\n`);
|
|
144
|
+
try {
|
|
145
|
+
sessionResult = await runInteractiveSession({
|
|
146
|
+
projectRoot,
|
|
147
|
+
scan,
|
|
148
|
+
reqsForDesignShapes: tree.requirements ?? [],
|
|
149
|
+
prompt,
|
|
150
|
+
write,
|
|
151
|
+
env: deps.env ?? process.env,
|
|
152
|
+
});
|
|
153
|
+
} finally {
|
|
154
|
+
rl.close();
|
|
155
|
+
}
|
|
156
|
+
} else if (flags.input) {
|
|
157
|
+
const raw = await readFile(flags.input, 'utf8');
|
|
158
|
+
const parsedInput = JSON.parse(raw);
|
|
159
|
+
sessionResult = normaliseNonInteractiveInput(parsedInput);
|
|
160
|
+
} else {
|
|
161
|
+
stderr.write('[error] usage preflight: not on a TTY and no --input file given\n');
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.message === 'preflight: session cancelled by operator') {
|
|
166
|
+
stderr.write('[error] preflight: session cancelled; no record written.\n');
|
|
167
|
+
return 4;
|
|
168
|
+
}
|
|
169
|
+
writeUnexpectedFailure(
|
|
170
|
+
rcfError({ kind: 'ioFailure', message: `preflight: ${err.message}`, stack: err.stack }),
|
|
171
|
+
stderr,
|
|
172
|
+
);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const now = deps.now ? new Date(deps.now()) : new Date();
|
|
177
|
+
const record = composePreflightRecord({
|
|
178
|
+
manifest: tree.manifest,
|
|
179
|
+
prdId,
|
|
180
|
+
services: sessionResult.services,
|
|
181
|
+
designShapeAnswers: sessionResult.designShapeAnswers,
|
|
182
|
+
now,
|
|
183
|
+
});
|
|
184
|
+
const optOuts = composeDesignShapeOptOuts({
|
|
185
|
+
manifest: tree.manifest,
|
|
186
|
+
preflightRecord: record,
|
|
187
|
+
designShapeAnswers: sessionResult.designShapeAnswers,
|
|
188
|
+
now,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (flags['dry-run']) {
|
|
192
|
+
if (flags.json) stdout.write(`${JSON.stringify({ record, optOuts }, null, 2)}\n`);
|
|
193
|
+
if (!flags.quiet) {
|
|
194
|
+
stdout.write(`[dry-run] preflight would write record ${record.id} (${record.servicesInScope.length} services, ${record.designShapeAnswers?.length ?? 0} design-shape answers, ${optOuts.length} opt-outs)\n`);
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const writeResult = await writePreflightRecord({
|
|
200
|
+
projectRoot, tree, record, optOuts, options: {},
|
|
201
|
+
});
|
|
202
|
+
if (writeResult && 'kind' in writeResult && 'message' in writeResult) {
|
|
203
|
+
if (writeResult.kind === 'ioFailure') { writeUnexpectedFailure(writeResult, stderr); return 1; }
|
|
204
|
+
stderr.write(`[error] ${writeResult.kind} ${writeResult.message}\n`);
|
|
205
|
+
if (writeResult.kind === 'usage') return 2;
|
|
206
|
+
if (writeResult.kind === 'validation' || writeResult.kind === 'brokenReference') return 3;
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (flags.json) stdout.write(`${JSON.stringify({ record, optOuts }, null, 2)}\n`);
|
|
211
|
+
if (!flags.quiet) {
|
|
212
|
+
stdout.write(`preflight: wrote ${record.id} to rcf/manifest.json (${record.servicesInScope.length} services, ${record.designShapeAnswers?.length ?? 0} design-shape answers)`);
|
|
213
|
+
if (optOuts.length > 0) stdout.write(` and ${optOuts.length} baseline opt-out(s)`);
|
|
214
|
+
stdout.write('.\n');
|
|
215
|
+
if (writeResult.skippedUiBaseline && (record.designShapeAnswers ?? []).length > 0) {
|
|
216
|
+
stdout.write(' Note: uiBaseline defaults writes are fenced pending Track B (UI design gate). The opt-out ledger carries the linkedPreFlightConfigRef for Track B to consume when it lands.\n');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
package/src/cli/read.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// `rcf read <id>` subcommand handler. Prints the document body (or a
|
|
2
|
+
// dot-path field). Inline AC / TC ids resolve to the addressed inline
|
|
3
|
+
// entry inside the parent US / TS body. Phase 4 §D7.
|
|
4
|
+
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
|
|
7
|
+
import { walkTree } from '#core/store';
|
|
8
|
+
import { findProjectRoot } from '../view/index.js';
|
|
9
|
+
|
|
10
|
+
const OPTION_SPEC = {
|
|
11
|
+
field: { type: 'string' },
|
|
12
|
+
raw: { type: 'boolean' },
|
|
13
|
+
help: { type: 'boolean' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const HELP = `Usage: rcf read <id> [options]
|
|
17
|
+
|
|
18
|
+
Prints the document body to stdout and a schema-validity report to
|
|
19
|
+
stderr: a valid document is reported as valid; an invalid-but-present
|
|
20
|
+
document is still returned, together with its validation errors, so the
|
|
21
|
+
caller sees both the content and the problem. The exit code stays 0
|
|
22
|
+
either way - read is a retrieval verb, not a gate (that is rcf validate).
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--field <dotPath> Print only the addressed field
|
|
26
|
+
--raw Emit unformatted (single-line) JSON
|
|
27
|
+
--help Print this help
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string[]} argv - argv slice after `read`
|
|
32
|
+
* @param {object} [deps]
|
|
33
|
+
* @returns {Promise<number>}
|
|
34
|
+
*/
|
|
35
|
+
export async function main(argv, deps = {}) {
|
|
36
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
37
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
38
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
39
|
+
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
45
|
+
stderr.write(HELP);
|
|
46
|
+
return 2;
|
|
47
|
+
}
|
|
48
|
+
const flags = parsed.values;
|
|
49
|
+
const positionals = parsed.positionals;
|
|
50
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
51
|
+
if (positionals.length !== 1) {
|
|
52
|
+
stderr.write('[error] usage read: expected exactly one <id>\n');
|
|
53
|
+
stderr.write(HELP);
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
const id = positionals[0];
|
|
57
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
58
|
+
if (!projectRoot) {
|
|
59
|
+
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');
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
63
|
+
const target = resolveTarget(tree, id);
|
|
64
|
+
if (!target) {
|
|
65
|
+
stderr.write(`[error] usage read: id ${id} not found\n`);
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
let value = target.doc;
|
|
69
|
+
if (flags.field) {
|
|
70
|
+
value = extractField(target.doc, flags.field);
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
stderr.write(`[error] usage read: field ${flags.field} not present on ${id}\n`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const out = flags.raw ? JSON.stringify(value) : JSON.stringify(value, null, 2);
|
|
77
|
+
stdout.write(`${out}\n`);
|
|
78
|
+
// Validity report (AC-301-1 / AC-301-3): the body went to stdout; the
|
|
79
|
+
// schema verdict goes to stderr so piped output stays pure JSON. An
|
|
80
|
+
// invalid-but-present document is returned WITH its validation errors -
|
|
81
|
+
// content and problem together. Read never gates on validity (exit 0);
|
|
82
|
+
// rcf validate is the gate.
|
|
83
|
+
if (target.invalid) {
|
|
84
|
+
stderr.write(`[warn] read: ${target.containerId} fails schema validation; body returned together with the errors:\n`);
|
|
85
|
+
const validationErrors = (errors ?? []).filter(
|
|
86
|
+
(e) => e.kind === 'validation' && e.documentId === target.containerId,
|
|
87
|
+
);
|
|
88
|
+
for (const e of validationErrors) {
|
|
89
|
+
stderr.write(`[warn] validation ${target.containerId}: ${e.message}\n`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
stderr.write(`[ok] read: ${id} is valid against its schema\n`);
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a target id against the tree. Supports:
|
|
99
|
+
* - root docs (PRD, TAD, BS, MANIFEST)
|
|
100
|
+
* - child docs (REQ, US, TAC, ADR, FBS, TS)
|
|
101
|
+
* - inline AC (`AC-XXX-N`) -> returns the AC entry from parent US
|
|
102
|
+
* - inline TC (`TC-XXX-slug`) -> returns the TC entry from parent TS
|
|
103
|
+
*/
|
|
104
|
+
function resolveTarget(tree, id) {
|
|
105
|
+
if (id === 'MANIFEST' && tree.manifest) {
|
|
106
|
+
return { doc: tree.manifest, containerId: 'MANIFEST', invalid: false };
|
|
107
|
+
}
|
|
108
|
+
const doc = tree.byId.get(id);
|
|
109
|
+
if (doc) return { doc, containerId: id, invalid: false };
|
|
110
|
+
// B5: schema-invalid docs stay addressable (tree.invalidDocs) - read
|
|
111
|
+
// returns the body together with the validation errors (AC-301-3).
|
|
112
|
+
const invalidEntry = tree.invalidDocs?.get(id);
|
|
113
|
+
if (invalidEntry) return { doc: invalidEntry.doc, containerId: id, invalid: true };
|
|
114
|
+
if (/^AC-\d+(-\d+)?$/.test(id)) {
|
|
115
|
+
const parentId = tree.parentByChild.get(id);
|
|
116
|
+
if (!parentId) return null;
|
|
117
|
+
const us = tree.byId.get(parentId);
|
|
118
|
+
if (!us) return null;
|
|
119
|
+
const entry = (us.acceptanceCriteria ?? []).find((ac) => ac.id === id);
|
|
120
|
+
return entry ? { doc: entry, containerId: parentId, invalid: false } : null;
|
|
121
|
+
}
|
|
122
|
+
if (/^TC-\d{3}-[a-z0-9-]+$/.test(id)) {
|
|
123
|
+
const parentId = tree.parentByChild.get(id);
|
|
124
|
+
if (!parentId) return null;
|
|
125
|
+
const ts = tree.byId.get(parentId);
|
|
126
|
+
if (!ts) return null;
|
|
127
|
+
const entry = (ts.testCases ?? []).find((tc) => tc.id === id);
|
|
128
|
+
return entry ? { doc: entry, containerId: parentId, invalid: false } : null;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractField(root, path) {
|
|
134
|
+
const parts = parseDotPath(path);
|
|
135
|
+
if (!parts) return undefined;
|
|
136
|
+
let cur = root;
|
|
137
|
+
for (const seg of parts) {
|
|
138
|
+
if (cur === undefined || cur === null) return undefined;
|
|
139
|
+
if (seg.kind === 'index') {
|
|
140
|
+
if (!Array.isArray(cur)) return undefined;
|
|
141
|
+
cur = cur[seg.value];
|
|
142
|
+
} else {
|
|
143
|
+
cur = cur[seg.value];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return cur;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseDotPath(path) {
|
|
150
|
+
if (typeof path !== 'string' || path.length === 0) return null;
|
|
151
|
+
const parts = [];
|
|
152
|
+
for (const seg of path.split('.')) {
|
|
153
|
+
const m = /^([^\[\]]+)((?:\[\d+\])*)$/.exec(seg);
|
|
154
|
+
if (!m) return null;
|
|
155
|
+
parts.push({ kind: 'prop', value: m[1] });
|
|
156
|
+
if (m[2]) {
|
|
157
|
+
const indices = m[2].match(/\d+/g) ?? [];
|
|
158
|
+
for (const n of indices) parts.push({ kind: 'index', value: Number(n) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return parts;
|
|
162
|
+
}
|