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,230 @@
|
|
|
1
|
+
// Baseline-AC sweep planner + applier (spec §5.3, §5.4).
|
|
2
|
+
//
|
|
3
|
+
// The "open sweep candidate" property is DERIVED from the tree, not
|
|
4
|
+
// persisted: a candidate is open exactly when the parent REQ carries a
|
|
5
|
+
// shape, the baselineKey is not present as an AC on the US, and no
|
|
6
|
+
// baselineAcOptOuts record covers it. See open-candidates.js. This module
|
|
7
|
+
// walks the tree via that derivation, presents the candidates for
|
|
8
|
+
// operator resolution, and materialises the two durable outcomes:
|
|
9
|
+
// accepted keys become ACs (authoredBy: baseline), opted-out keys become
|
|
10
|
+
// baselineAcOptOuts entries. Skipping (or being silent) simply leaves
|
|
11
|
+
// the derived-open state as-is; the Stage-1 gate (gate.js) refuses build
|
|
12
|
+
// on any FBS binding an AC on a US that still derives an open key.
|
|
13
|
+
//
|
|
14
|
+
// Moment-4 non-interactive create (spec §5.3 moment 4): a US created
|
|
15
|
+
// under a classified REQ does not need any explicit auto-enqueue write
|
|
16
|
+
// because the derivation catches unresolved keys directly. The Stage-1
|
|
17
|
+
// gate remains the single unbypassable refusal surface.
|
|
18
|
+
|
|
19
|
+
import { updateDocument } from '#core/store';
|
|
20
|
+
import {
|
|
21
|
+
BASELINE_SHAPE_KEYS,
|
|
22
|
+
getBaselineSet,
|
|
23
|
+
} from '#core/baseline-catalog';
|
|
24
|
+
|
|
25
|
+
import { writeOptOut } from './opt-out.js';
|
|
26
|
+
import { openCandidatesForUs } from './open-candidates.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Plan the baseline sweep across the tree.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
* @param {string|null} [opts.reqId]
|
|
34
|
+
* @param {boolean} [opts.all]
|
|
35
|
+
* @returns {{ candidates: Array<{ usId: string, reqId: string, baselineKey: string, reqShape: string, canonicalText: string }> }}
|
|
36
|
+
*/
|
|
37
|
+
export function planSweep(tree, opts = {}) {
|
|
38
|
+
const { reqId = null, all = false } = opts;
|
|
39
|
+
|
|
40
|
+
/** @type {Array<{ usId: string, reqId: string, baselineKey: string, reqShape: string, canonicalText: string }>} */
|
|
41
|
+
const candidates = [];
|
|
42
|
+
|
|
43
|
+
for (const [id, kind] of tree.kindById) {
|
|
44
|
+
if (kind !== 'userStory') continue;
|
|
45
|
+
const us = tree.byId.get(id);
|
|
46
|
+
if (!us) continue;
|
|
47
|
+
if (!all && reqId && us.reqId !== reqId) continue;
|
|
48
|
+
if (!all && !reqId) continue;
|
|
49
|
+
for (const c of openCandidatesForUs(tree, us)) {
|
|
50
|
+
candidates.push(c);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { candidates };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apply a batch of decisions.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} args
|
|
60
|
+
* @param {string} args.projectRoot
|
|
61
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
62
|
+
* @param {Array<{ candidate: object, action: 'accept'|'opt-out'|'skip', reason?: string }>} args.decisions
|
|
63
|
+
* @param {Date} [args.now]
|
|
64
|
+
* @returns {Promise<{ accepted: number, optedOut: number, left: number, optOutIds: string[], writtenAcIds: string[] } | import('#core/errors').RcfError>}
|
|
65
|
+
*/
|
|
66
|
+
export async function applySweepDecisions({ projectRoot, tree, decisions, now = new Date() }) {
|
|
67
|
+
let accepted = 0;
|
|
68
|
+
let optedOut = 0;
|
|
69
|
+
let left = 0;
|
|
70
|
+
/** @type {string[]} */
|
|
71
|
+
const optOutIds = [];
|
|
72
|
+
/** @type {string[]} */
|
|
73
|
+
const writtenAcIds = [];
|
|
74
|
+
|
|
75
|
+
// Group by US so each US gets one write.
|
|
76
|
+
const byUs = new Map();
|
|
77
|
+
for (const d of decisions) {
|
|
78
|
+
const usId = d.candidate.usId;
|
|
79
|
+
if (!byUs.has(usId)) byUs.set(usId, []);
|
|
80
|
+
byUs.get(usId).push(d);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @type {Array<{ reqId: string, baselineKey: string, reason: string }>} */
|
|
84
|
+
const pendingOptOuts = [];
|
|
85
|
+
|
|
86
|
+
for (const [usId, list] of byUs) {
|
|
87
|
+
const usDoc = tree.byId.get(usId);
|
|
88
|
+
if (!usDoc) continue;
|
|
89
|
+
|
|
90
|
+
const acsToAppend = [];
|
|
91
|
+
for (const d of list) {
|
|
92
|
+
if (d.action === 'accept') {
|
|
93
|
+
const ac = composeBaselineAc({ usDoc, candidate: d.candidate, now });
|
|
94
|
+
acsToAppend.push(ac);
|
|
95
|
+
writtenAcIds.push(ac.id);
|
|
96
|
+
accepted += 1;
|
|
97
|
+
} else if (d.action === 'opt-out') {
|
|
98
|
+
pendingOptOuts.push({ reqId: d.candidate.reqId, baselineKey: d.candidate.baselineKey, reason: d.reason ?? '' });
|
|
99
|
+
optedOut += 1;
|
|
100
|
+
} else {
|
|
101
|
+
left += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (acsToAppend.length === 0) continue;
|
|
106
|
+
|
|
107
|
+
const nextAcs = [
|
|
108
|
+
...(Array.isArray(usDoc.acceptanceCriteria) ? usDoc.acceptanceCriteria : []),
|
|
109
|
+
...acsToAppend,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const result = await updateDocument({
|
|
113
|
+
projectRoot,
|
|
114
|
+
tree,
|
|
115
|
+
id: usId,
|
|
116
|
+
patch: { acceptanceCriteria: nextAcs },
|
|
117
|
+
sets: [],
|
|
118
|
+
options: {},
|
|
119
|
+
walkErrors: [],
|
|
120
|
+
});
|
|
121
|
+
if (result && result.kind && typeof result.message === 'string') return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const p of pendingOptOuts) {
|
|
125
|
+
const out = await writeOptOut({
|
|
126
|
+
projectRoot,
|
|
127
|
+
tree,
|
|
128
|
+
reqId: p.reqId,
|
|
129
|
+
baselineKey: p.baselineKey,
|
|
130
|
+
reason: p.reason,
|
|
131
|
+
scope: 'req',
|
|
132
|
+
now,
|
|
133
|
+
});
|
|
134
|
+
if (out && out.kind && typeof out.message === 'string') return out;
|
|
135
|
+
optOutIds.push(out.id);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { accepted, optedOut, left, optOutIds, writtenAcIds };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Moment-4 auto-enqueue is a no-op today: the openness of a candidate is
|
|
143
|
+
* derived by open-candidates.js from the tree state (shape-classified
|
|
144
|
+
* REQ, no matching AC, no matching opt-out). A fresh US under a
|
|
145
|
+
* classified REQ therefore surfaces every unresolved baseline key as
|
|
146
|
+
* OPEN automatically, without a separate queue-write. The Stage-1 gate
|
|
147
|
+
* (gate.js) reads the same derivation and refuses build accordingly.
|
|
148
|
+
*
|
|
149
|
+
* Kept exported so the create/us hook has a stable seam name and can
|
|
150
|
+
* grow a queue-write in a future schemas release when a persistent
|
|
151
|
+
* open-queue field lands on user-story.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} args
|
|
154
|
+
* @param {string} args.projectRoot
|
|
155
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
156
|
+
* @param {string} args.usId
|
|
157
|
+
* @returns {Promise<{ enqueued: string[] }>}
|
|
158
|
+
*/
|
|
159
|
+
export async function applyPendingBaselinesForUs({ tree, usId }) {
|
|
160
|
+
const usDoc = tree.byId.get(usId);
|
|
161
|
+
if (!usDoc) return { enqueued: [] };
|
|
162
|
+
const open = openCandidatesForUs(tree, usDoc);
|
|
163
|
+
return { enqueued: open.map((c) => c.baselineKey) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Compose an AC to inject for a baseline candidate.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} args
|
|
170
|
+
* @param {object} args.usDoc
|
|
171
|
+
* @param {{ baselineKey: string, reqShape: string, canonicalText: string, given?: string, when?: string, then?: string }} args.candidate
|
|
172
|
+
* @param {Date} args.now
|
|
173
|
+
* @returns {object}
|
|
174
|
+
*/
|
|
175
|
+
export function composeBaselineAc({ usDoc, candidate, now }) {
|
|
176
|
+
const isoNow = now.toISOString();
|
|
177
|
+
const id = nextAcId(usDoc);
|
|
178
|
+
const canonicalText = candidate.canonicalText;
|
|
179
|
+
const gwt = candidate.given || candidate.when || candidate.then
|
|
180
|
+
? { given: candidate.given ?? '', when: candidate.when ?? '', then: candidate.then ?? '' }
|
|
181
|
+
: extractGwt(canonicalText);
|
|
182
|
+
const ac = {
|
|
183
|
+
id,
|
|
184
|
+
description: canonicalText,
|
|
185
|
+
testable: true,
|
|
186
|
+
provenance: {
|
|
187
|
+
authoredBy: 'baseline',
|
|
188
|
+
baselineKey: candidate.baselineKey,
|
|
189
|
+
injectedAt: isoNow,
|
|
190
|
+
sourceReqShape: candidate.reqShape,
|
|
191
|
+
acceptedByOperatorAt: isoNow,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
if (gwt.given) ac.given = gwt.given;
|
|
195
|
+
if (gwt.when) ac.when = gwt.when;
|
|
196
|
+
if (gwt.then) ac.then = gwt.then;
|
|
197
|
+
return ac;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const AC_HIER_ID_RE = /^AC-(\d{3,})-(\d+)$/;
|
|
201
|
+
|
|
202
|
+
function nextAcId(usDoc) {
|
|
203
|
+
const acs = Array.isArray(usDoc.acceptanceCriteria) ? usDoc.acceptanceCriteria : [];
|
|
204
|
+
// Prefer hierarchical form matching the US id, e.g. US-201 → AC-201-1.
|
|
205
|
+
const usNum = (usDoc.usId ?? '').match(/^US-(\d+)$/)?.[1];
|
|
206
|
+
let maxN = 0;
|
|
207
|
+
for (const ac of acs) {
|
|
208
|
+
const m = (ac?.id ?? '').match(AC_HIER_ID_RE);
|
|
209
|
+
if (!m) continue;
|
|
210
|
+
const [, hier, tail] = m;
|
|
211
|
+
if (usNum && hier !== usNum) continue;
|
|
212
|
+
const n = Number.parseInt(tail, 10);
|
|
213
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
214
|
+
}
|
|
215
|
+
return usNum ? `AC-${usNum}-${maxN + 1}` : `AC-${(Math.floor(Math.random() * 999)).toString().padStart(3, '0')}-1`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function extractGwt(text) {
|
|
219
|
+
const gwt = { given: '', when: '', then: '' };
|
|
220
|
+
const g = text.match(/given\s+(.+?),\s*when/i);
|
|
221
|
+
const w = text.match(/,\s*when\s+(.+?),\s*then/i);
|
|
222
|
+
const t = text.match(/,\s*then\s+(.+)$/i);
|
|
223
|
+
if (g) gwt.given = g[1].trim();
|
|
224
|
+
if (w) gwt.when = w[1].trim();
|
|
225
|
+
if (t) gwt.then = t[1].trim().replace(/\.\s*$/, '');
|
|
226
|
+
return gwt;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Kept-exported catalog constants for downstream discoverability.
|
|
230
|
+
void BASELINE_SHAPE_KEYS; void getBaselineSet;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// REQ-shape classifier (Track C+D spec §4.4).
|
|
2
|
+
//
|
|
3
|
+
// Deterministic keyword-scan over an already-loaded REQ document. Reads
|
|
4
|
+
// `title`, `description`, `rationale`, folds the parent PRD's `intent`
|
|
5
|
+
// and `problem` as fallback context, and returns a
|
|
6
|
+
// `shapeClassification` block per the requirement schema (§3.1). Never
|
|
7
|
+
// reads USes or ACs (they may not exist yet and would circularly
|
|
8
|
+
// re-trigger the classifier).
|
|
9
|
+
//
|
|
10
|
+
// Signals come from `matchReqShapeSignals` in core (single source of
|
|
11
|
+
// truth; Track B's UI patterns flow through the same file). Multi-shape
|
|
12
|
+
// verdicts are legitimate; the classifier records each match tagged with
|
|
13
|
+
// the source field it came from.
|
|
14
|
+
//
|
|
15
|
+
// Content-pending case: a REQ whose `description` is empty at write time
|
|
16
|
+
// records `shapes: []`, `reason: content-pending`; the classifier reruns
|
|
17
|
+
// on the first `rcf update req --description ...` that adds real content.
|
|
18
|
+
|
|
19
|
+
import { matchReqShapeSignals, SHAPE_KEYS } from '#core/patterns/req-shapes';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {'webUi'|'httpApi'|'auth'|'persistence'|'notifications'} ReqShape
|
|
23
|
+
* @typedef {'webUi'|'httpApi'|'auth'|'persistence'|'notifications'|'none'} ReqShapeWithNone
|
|
24
|
+
* @typedef {'keyword-scan'|'content-pending'|'inheritedFromParent'|'operatorOverride'} ClassifierReason
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} ReqShapeSignal
|
|
29
|
+
* @property {'title'|'description'|'rationale'} source
|
|
30
|
+
* @property {string} match
|
|
31
|
+
* @property {ReqShape} shape
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} ReqShapeClassification
|
|
36
|
+
* @property {ReqShapeWithNone[]} shapes
|
|
37
|
+
* @property {ClassifierReason} reason
|
|
38
|
+
* @property {ReqShapeSignal[]} [signals]
|
|
39
|
+
* @property {string} classifiedAt
|
|
40
|
+
* @property {object} [operatorOverride]
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const ALLOWED_SOURCES = Object.freeze(['title', 'description', 'rationale']);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute the shape classification for a REQ. Pure function; does not
|
|
47
|
+
* write to the document. Callers merge the returned block onto the REQ
|
|
48
|
+
* before persisting.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} reqDoc the REQ document body
|
|
51
|
+
* @param {object} [opts]
|
|
52
|
+
* @param {object} [opts.parentPrd] parent PRD body for fallback context
|
|
53
|
+
* @param {Date} [opts.now] clock injection for tests
|
|
54
|
+
* @returns {ReqShapeClassification}
|
|
55
|
+
*/
|
|
56
|
+
export function classifyReq(reqDoc, opts = {}) {
|
|
57
|
+
const now = opts.now ?? new Date();
|
|
58
|
+
const isoNow = now.toISOString();
|
|
59
|
+
|
|
60
|
+
const title = typeof reqDoc?.title === 'string' ? reqDoc.title : '';
|
|
61
|
+
const description = typeof reqDoc?.description === 'string' ? reqDoc.description : '';
|
|
62
|
+
const rationale = typeof reqDoc?.rationale === 'string' ? reqDoc.rationale : '';
|
|
63
|
+
|
|
64
|
+
// Content-pending: the REQ has no real description body yet. Placeholder
|
|
65
|
+
// scaffolds land as "TODO", so treat descriptions that are empty or
|
|
66
|
+
// exactly-a-todo as pending. The classifier re-runs on the next update
|
|
67
|
+
// once the operator or an intake pass fills in real prose.
|
|
68
|
+
const contentPending = description.length === 0 || /^\s*TODO\b/i.test(description);
|
|
69
|
+
if (contentPending) {
|
|
70
|
+
return {
|
|
71
|
+
shapes: [],
|
|
72
|
+
reason: 'content-pending',
|
|
73
|
+
classifiedAt: isoNow,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const signals = [];
|
|
78
|
+
|
|
79
|
+
const primarySources = [
|
|
80
|
+
{ source: 'title', text: title },
|
|
81
|
+
{ source: 'description', text: description },
|
|
82
|
+
{ source: 'rationale', text: rationale },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const { source, text } of primarySources) {
|
|
86
|
+
if (!ALLOWED_SOURCES.includes(source) || text.length === 0) continue;
|
|
87
|
+
for (const m of matchReqShapeSignals(text)) {
|
|
88
|
+
signals.push({ source, shape: m.shape, match: m.match });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parent PRD fallback: only used to seed additional shapes when the REQ
|
|
93
|
+
// itself did not surface one. Signals are still attributed to the REQ's
|
|
94
|
+
// own fields per the schema enum (title/description/rationale) so the
|
|
95
|
+
// signals array shape stays stable; a shape inherited from the PRD lands
|
|
96
|
+
// in `shapes[]` with no signal row and the classifier reason stays
|
|
97
|
+
// keyword-scan (the signals are still what the REQ said; the parent
|
|
98
|
+
// widens the shape list only when the REQ's own text was silent). This
|
|
99
|
+
// matches spec §4.4 "fallback context" without mixing scopes.
|
|
100
|
+
const parentPrd = opts.parentPrd ?? null;
|
|
101
|
+
const parentText = parentPrd
|
|
102
|
+
? [parentPrd.intent, parentPrd.problem].filter((s) => typeof s === 'string' && s.length > 0).join(' ')
|
|
103
|
+
: '';
|
|
104
|
+
const parentSignals = parentText.length > 0 ? matchReqShapeSignals(parentText) : [];
|
|
105
|
+
|
|
106
|
+
// Merge shapes: any shape the REQ's own text matched, plus any shape
|
|
107
|
+
// the parent PRD adds. Preserved in canonical order.
|
|
108
|
+
const shapesSet = new Set();
|
|
109
|
+
for (const s of signals) shapesSet.add(s.shape);
|
|
110
|
+
for (const m of parentSignals) shapesSet.add(m.shape);
|
|
111
|
+
|
|
112
|
+
const shapes = SHAPE_KEYS.filter((s) => shapesSet.has(s));
|
|
113
|
+
const finalShapes = shapes.length === 0 ? ['none'] : shapes;
|
|
114
|
+
|
|
115
|
+
const block = {
|
|
116
|
+
shapes: finalShapes,
|
|
117
|
+
reason: 'keyword-scan',
|
|
118
|
+
classifiedAt: isoNow,
|
|
119
|
+
};
|
|
120
|
+
if (signals.length > 0) block.signals = signals;
|
|
121
|
+
return block;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Merge a fresh classification onto an existing REQ document. Preserves
|
|
126
|
+
* a previous `operatorOverride` block when present, and preserves
|
|
127
|
+
* signals from earlier runs when the new run comes back
|
|
128
|
+
* `content-pending` (never clear real signals with a pending re-run;
|
|
129
|
+
* pending is a "no verdict yet" state, not a delete).
|
|
130
|
+
*
|
|
131
|
+
* @param {object} reqDoc REQ document to merge onto (not mutated)
|
|
132
|
+
* @param {ReqShapeClassification} block classification from `classifyReq`
|
|
133
|
+
* @returns {object} new REQ doc with the merged block
|
|
134
|
+
*/
|
|
135
|
+
export function mergeClassificationOntoReq(reqDoc, block) {
|
|
136
|
+
const existing = reqDoc?.shapeClassification;
|
|
137
|
+
// On content-pending: only apply if there is no previous verdict.
|
|
138
|
+
if (block.reason === 'content-pending' && existing && existing.reason !== 'content-pending') {
|
|
139
|
+
return reqDoc;
|
|
140
|
+
}
|
|
141
|
+
const next = { ...reqDoc, shapeClassification: { ...block } };
|
|
142
|
+
if (existing?.operatorOverride) {
|
|
143
|
+
next.shapeClassification.operatorOverride = existing.operatorOverride;
|
|
144
|
+
// An operator override wins on the shapes field.
|
|
145
|
+
if (Array.isArray(existing.operatorOverride.newShapes) && existing.operatorOverride.newShapes.length > 0) {
|
|
146
|
+
next.shapeClassification.shapes = [...existing.operatorOverride.newShapes];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Compose an operator override block per spec §4.5. Records both the
|
|
154
|
+
* original classifier shapes and the newly-ruled shapes so the override
|
|
155
|
+
* history is visible to reviewers. Does not mutate the input; returns a
|
|
156
|
+
* new `shapeClassification` block ready to write.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} args
|
|
159
|
+
* @param {ReqShapeWithNone[]} args.originalShapes
|
|
160
|
+
* @param {ReqShapeWithNone[]} args.newShapes
|
|
161
|
+
* @param {string} args.reason
|
|
162
|
+
* @param {ReqShapeSignal[]} [args.signals]
|
|
163
|
+
* @param {Date} [args.now]
|
|
164
|
+
* @returns {ReqShapeClassification}
|
|
165
|
+
*/
|
|
166
|
+
export function composeOperatorOverride({ originalShapes, newShapes, reason, signals, now = new Date() }) {
|
|
167
|
+
const isoNow = now.toISOString();
|
|
168
|
+
const block = {
|
|
169
|
+
shapes: [...newShapes],
|
|
170
|
+
reason: 'operatorOverride',
|
|
171
|
+
classifiedAt: isoNow,
|
|
172
|
+
operatorOverride: {
|
|
173
|
+
originalShapes: [...originalShapes],
|
|
174
|
+
newShapes: [...newShapes],
|
|
175
|
+
reason,
|
|
176
|
+
ackAt: isoNow,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
if (Array.isArray(signals) && signals.length > 0) block.signals = signals;
|
|
180
|
+
return block;
|
|
181
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Persist a REQ-shape classification onto a REQ document (Track C+D §4.4).
|
|
2
|
+
//
|
|
3
|
+
// Reads the REQ from the in-memory tree, computes classification via
|
|
4
|
+
// classifyReq (folding parent PRD context when the PRD is loaded), merges
|
|
5
|
+
// per mergeClassificationOntoReq (which respects operator overrides and
|
|
6
|
+
// refuses to overwrite a real verdict with content-pending), and writes
|
|
7
|
+
// the merged shapeClassification block back via updateDocument.
|
|
8
|
+
//
|
|
9
|
+
// Never fails the calling verb: a classification write error is warned
|
|
10
|
+
// on stderr but returns success (the write we care about is the REQ's
|
|
11
|
+
// own body write; classification is provenance layered on top).
|
|
12
|
+
|
|
13
|
+
import { updateDocument } from '#core/store';
|
|
14
|
+
import { classifyReq, mergeClassificationOntoReq } from './classifier.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} args
|
|
18
|
+
* @param {string} args.projectRoot
|
|
19
|
+
* @param {import('#core/store/walker.js').TreeModel} args.tree
|
|
20
|
+
* @param {string} args.reqId the id of the REQ to classify
|
|
21
|
+
* @param {Date} [args.now]
|
|
22
|
+
* @returns {Promise<{ ok: true, block: object, changed: boolean } | { ok: false, message: string }>}
|
|
23
|
+
*/
|
|
24
|
+
export async function classifyAndPersistReq({ projectRoot, tree, reqId, now = new Date() }) {
|
|
25
|
+
const reqDoc = tree?.byId?.get(reqId);
|
|
26
|
+
if (!reqDoc || reqDoc.reqId !== reqId) {
|
|
27
|
+
return { ok: false, message: `req-classify: ${reqId} not found in tree` };
|
|
28
|
+
}
|
|
29
|
+
const prdId = reqDoc.prdId;
|
|
30
|
+
const parentPrd = prdId ? tree.byId.get(prdId) : null;
|
|
31
|
+
|
|
32
|
+
const block = classifyReq(reqDoc, { parentPrd: parentPrd ?? undefined, now });
|
|
33
|
+
const merged = mergeClassificationOntoReq(reqDoc, block);
|
|
34
|
+
|
|
35
|
+
// Nothing to write when the merge decided to keep the existing block.
|
|
36
|
+
if (merged === reqDoc) return { ok: true, block: reqDoc.shapeClassification, changed: false };
|
|
37
|
+
|
|
38
|
+
const nextBlock = merged.shapeClassification;
|
|
39
|
+
// Write back through the store's updateDocument path so validation
|
|
40
|
+
// fires and updatedAt bumps.
|
|
41
|
+
const result = await updateDocument({
|
|
42
|
+
projectRoot,
|
|
43
|
+
tree,
|
|
44
|
+
id: reqId,
|
|
45
|
+
patch: { shapeClassification: nextBlock },
|
|
46
|
+
sets: [],
|
|
47
|
+
options: {},
|
|
48
|
+
walkErrors: [],
|
|
49
|
+
});
|
|
50
|
+
if (result && result.kind && typeof result.message === 'string') {
|
|
51
|
+
// RcfError shape
|
|
52
|
+
return { ok: false, message: `req-classify: ${result.message}` };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, block: nextBlock, changed: true };
|
|
55
|
+
}
|