rcf-lite 0.30.0 → 0.31.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 +46 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +15 -10
- package/package.json +1 -1
- package/rcf/adrs/adr-4131-gates-bite-ack-recorded-override.json +3 -3
- package/rcf/adrs/adr-4137-standalone-theme-persistence.json +30 -0
- package/rcf/adrs/adr-4138-tree-wide-checks-before-scope-shortcircuit.json +35 -0
- package/rcf/code-nodes/cn-771.json +14 -0
- package/rcf/code-nodes/cn-772.json +14 -0
- package/rcf/code-nodes/cn-773.json +16 -0
- package/rcf/code-nodes/cn-774.json +20 -0
- package/rcf/code-nodes/cn-775.json +15 -0
- package/rcf/code-nodes/cn-776.json +17 -0
- package/rcf/code-nodes/cn-777.json +21 -0
- package/rcf/fbs/fbs-202.json +18 -2
- package/rcf/fbs/fbs-203.json +12 -4
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +2 -2
- package/rcf/tacs/tac-4122-define-gates.json +7 -2
- package/rcf/test-suites/ts-007.json +1 -1
- package/rcf/test-suites/ts-215.json +64 -1
- package/rcf/test-suites/ts-216.json +18 -2
- package/rcf/test-suites/ts-219.json +10 -2
- package/rcf/test-suites/ts-220.json +18 -2
- package/rcf/test-suites/ts-225.json +10 -2
- package/rcf/test-suites/ts-229.json +49 -3
- package/rcf/test-suites/ts-230.json +26 -2
- package/rcf/test-suites/ts-231.json +16 -1
- package/rcf/test-suites/ts-236.json +44 -0
- package/rcf/user-stories/us-17302.json +36 -1
- package/rcf/user-stories/us-17401.json +11 -1
- package/rcf/user-stories/us-17402.json +6 -1
- package/rcf/user-stories/us-17403.json +11 -1
- package/rcf/user-stories/us-17404.json +16 -1
- package/rcf/user-stories/us-17405.json +7 -2
- package/rcf/user-stories/us-205.json +6 -1
- package/rcf/user-stories/us-206.json +25 -4
- package/rcf/user-stories/us-207.json +36 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/create.js +22 -2
- package/src/cli/ledger.js +96 -4
- package/src/cli/readiness.js +15 -1
- package/src/core/errors/index.js +7 -1
- package/src/core/store/writer.js +181 -13
- package/src/define/ledgers.js +86 -7
- package/src/mcp/tools.js +1 -1
- package/src/query/gates.js +383 -150
- package/src/view/html-page.js +14 -2
- package/src/view/page-init.js +149 -8
- package/src/view/style.css +97 -18
- package/src/view/view-index.js +22 -0
package/src/query/gates.js
CHANGED
|
@@ -173,7 +173,7 @@ export const CHECK_QUESTION = /** @type {const} */ ({
|
|
|
173
173
|
'skeleton:reqShape': 'Every requirement in scope carries a shape classification.',
|
|
174
174
|
'skeleton:tadPersistence': 'TAD data architecture lists dataStores and coreEntities for persistence REQs.',
|
|
175
175
|
'skeleton:deployAdr': 'Exactly one Deploy target or Deploy deferral ADR exists.',
|
|
176
|
-
'skeleton:standardsCited': 'Every registered standards pack is cited
|
|
176
|
+
'skeleton:standardsCited': 'Every registered standards pack is cited, applied or waived.',
|
|
177
177
|
'shapes:tacHasInterface': 'Every TAC in scope has at least one interface.',
|
|
178
178
|
'shapes:kindVocabulary': 'Every interface kind is in the closed vocabulary.',
|
|
179
179
|
'shapes:draftSettled': 'Every draft interface has been settled (the [draft] marker is removed).',
|
|
@@ -378,16 +378,39 @@ export function parseInterfaceTemplate(iface) {
|
|
|
378
378
|
return { kind, missing };
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Known bare repo filenames the slash-or-extension rule would otherwise
|
|
383
|
+
* silently skip (ruling R6 2026-10-05). A `path: Dockerfile` on an
|
|
384
|
+
* interface description names a real file at the repo root, so the
|
|
385
|
+
* `shapes:pathsResolve` check should see it. Exported so the tests can
|
|
386
|
+
* assert the list verbatim; a bare word outside this set still fails
|
|
387
|
+
* the slash-or-extension rule as before.
|
|
388
|
+
*/
|
|
389
|
+
export const BARE_PATH_WHITELIST = /** @type {const} */ ([
|
|
390
|
+
'Dockerfile',
|
|
391
|
+
'Containerfile',
|
|
392
|
+
'Makefile',
|
|
393
|
+
'LICENSE',
|
|
394
|
+
'Procfile',
|
|
395
|
+
'Justfile',
|
|
396
|
+
'Rakefile',
|
|
397
|
+
'Gemfile',
|
|
398
|
+
'CODEOWNERS',
|
|
399
|
+
]);
|
|
400
|
+
const BARE_PATH_WHITELIST_SET = new Set(BARE_PATH_WHITELIST);
|
|
401
|
+
|
|
381
402
|
/**
|
|
382
403
|
* Rough `path:` token scanner for an interface description. Returns
|
|
383
404
|
* every candidate path token the description names so the CLI can
|
|
384
405
|
* resolve them on disk and the gate can decide `shapes:pathsResolve`
|
|
385
406
|
* against the resolved map (ADR-4131). A candidate is any text that
|
|
386
407
|
* follows a `path:` or `path =` marker and looks like a repo-relative
|
|
387
|
-
* path (contains a `/` or ends with a file extension,
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
408
|
+
* path (contains a `/` or ends with a file extension, or is one of the
|
|
409
|
+
* known bare repo names in BARE_PATH_WHITELIST per ruling R6
|
|
410
|
+
* 2026-10-05, no scheme). The scanner is deliberately conservative: an
|
|
411
|
+
* http URL, a bare word outside the whitelist, a glob or a template
|
|
412
|
+
* placeholder is not a candidate. Exported for the CLI's resolve-on-disk
|
|
413
|
+
* pass.
|
|
391
414
|
*
|
|
392
415
|
* @param {string} description
|
|
393
416
|
* @returns {string[]}
|
|
@@ -409,6 +432,11 @@ export function extractInterfacePathTokens(description) {
|
|
|
409
432
|
// `src/x.js` or `packages/rcf-lite/CHANGELOG.md`; those don't
|
|
410
433
|
// start with `/`.
|
|
411
434
|
if (token.startsWith('/') && !/\.[a-zA-Z0-9]+$/.test(token)) continue;
|
|
435
|
+
// Ruling R6 2026-10-05: whitelist the usual bare repo names.
|
|
436
|
+
if (BARE_PATH_WHITELIST_SET.has(token)) {
|
|
437
|
+
out.push(token);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
412
440
|
if (!/\//.test(token) && !/\.[a-zA-Z0-9]+$/.test(token)) continue;
|
|
413
441
|
out.push(token);
|
|
414
442
|
}
|
|
@@ -434,11 +462,13 @@ export function stagePolicy(stage) {
|
|
|
434
462
|
|
|
435
463
|
/**
|
|
436
464
|
* ADR-4131 (0.30.0 PR 5): the three gates that accept a recorded
|
|
437
|
-
* `--ack` override. `stagePolicy`
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
465
|
+
* `--ack` override. `stagePolicy(stage)` returns `warnWithAck` for
|
|
466
|
+
* D3/D5/D6 and `blocking` for everything else (ruling R4 2026-10-05
|
|
467
|
+
* retired the pre-R4 wording that said `stagePolicy` kept returning
|
|
468
|
+
* `blocking` for every stage); `ackable(stage)` is the companion
|
|
469
|
+
* signal the readiness and freeze CLIs read to decide whether a
|
|
470
|
+
* failing stage honours an acknowledgement at the current hash. True
|
|
471
|
+
* for D3, D5 and D6; false for everything else.
|
|
442
472
|
*
|
|
443
473
|
* @param {string} stage
|
|
444
474
|
* @returns {boolean}
|
|
@@ -753,6 +783,43 @@ function asSet(scope) {
|
|
|
753
783
|
return scope instanceof Set ? scope : new Set(scope ?? []);
|
|
754
784
|
}
|
|
755
785
|
|
|
786
|
+
/**
|
|
787
|
+
* ADR-4138 (DEFINE step 3 rulings R2, R5, R7 2026-10-05 and ruling R13
|
|
788
|
+
* 2026-10-05 PR 292 follow-up): run every tree-wide check for a stage
|
|
789
|
+
* and either return a stage envelope carrying only the tree-wide
|
|
790
|
+
* findings (plus the scope placeholder) folded through `foldState`, or
|
|
791
|
+
* null when every tree-wide check passed, letting the stage proceed to
|
|
792
|
+
* its own scope early-return or full checks list.
|
|
793
|
+
*
|
|
794
|
+
* The helper governs every tree-wide check: `shapes:draftSettled`
|
|
795
|
+
* (R2), `shapes:entityJoin` (R5), `skeleton:standardsCited` (R7),
|
|
796
|
+
* `skeleton:deployAdr` (R13), `crosscut:securityArchitecture` (R13)
|
|
797
|
+
* and `crosscut:operationalConcerns` (R13). Delta-only checks
|
|
798
|
+
* (templateMarkers, pathsResolve, closedSets, ownerRefResolves) are
|
|
799
|
+
* untouched: they stay on the stage's own full-checks path. The
|
|
800
|
+
* ack-at-current-hash fold is delegated to `foldState` so one
|
|
801
|
+
* implementation owns the acknowledgement rule and the failing
|
|
802
|
+
* envelope carries no stale `notApplicable` reason text (the pre-R13
|
|
803
|
+
* copy of the stage's notApplicable message misled the readiness CLI
|
|
804
|
+
* print line, which reads `stage.reason` on every state).
|
|
805
|
+
*
|
|
806
|
+
* @param {string} stage
|
|
807
|
+
* @param {string} gate
|
|
808
|
+
* @param {Array<{name: string, check: object}>} treeWideChecks
|
|
809
|
+
* @param {object} freezeCtx
|
|
810
|
+
* @returns {{ stage: string, gate: string, state: 'failing' | 'acknowledged', checks: object[] } | null}
|
|
811
|
+
*/
|
|
812
|
+
function treeWideFailureEnvelope(stage, gate, treeWideChecks, freezeCtx) {
|
|
813
|
+
const anyFailing = treeWideChecks.some(({ check }) => !check.ok);
|
|
814
|
+
if (!anyFailing) return null;
|
|
815
|
+
const checks = [makeCheck(`stage:${stage}:scope`, 'delta', 0, [])];
|
|
816
|
+
for (const { check } of treeWideChecks) checks.push(check);
|
|
817
|
+
// Delegate to foldState: it owns the warn-with-ack fold rule
|
|
818
|
+
// (ADR-4122). The returned envelope carries { state: 'failing' } or
|
|
819
|
+
// { state: 'acknowledged' } and no reason field.
|
|
820
|
+
return foldState(stage, gate, checks, freezeCtx);
|
|
821
|
+
}
|
|
822
|
+
|
|
756
823
|
// ---------------------------------------------------------------------------
|
|
757
824
|
// D1 -- Brief intake and ledger (blocking in 0.29.0).
|
|
758
825
|
// ---------------------------------------------------------------------------
|
|
@@ -862,6 +929,81 @@ export function checkD1Brief(ctx) {
|
|
|
862
929
|
/** Kinds whose resolution D2 checks: capability / constraint / entity / actor / externalSystem / surface. */
|
|
863
930
|
const D2_RESOLVING_KINDS = new Set(['capability', 'constraint', 'entity', 'actor', 'externalSystem', 'surface']);
|
|
864
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Compute the `skeleton:deployAdr` check (engineer, tree-wide). Pure.
|
|
934
|
+
* Ruling R13 2026-10-05 (ADR-4138 extended, PR 292 follow-up): exactly
|
|
935
|
+
* one Deploy target or Deploy deferral ADR exists tree-wide (detected
|
|
936
|
+
* by title prefix per proposal section 11). The check runs ahead of
|
|
937
|
+
* the D2 scope early-return so a narrowed scope that excludes every
|
|
938
|
+
* REQ / PRD / TAD / brief statement still reports a missing (or
|
|
939
|
+
* doubled) Deploy ADR.
|
|
940
|
+
*
|
|
941
|
+
* @param {StageContext} ctx
|
|
942
|
+
* @returns {ReturnType<typeof makeCheck>}
|
|
943
|
+
*/
|
|
944
|
+
function computeDeployAdrCheck(ctx) {
|
|
945
|
+
const tree = ctx.tree;
|
|
946
|
+
const deployAdrs = (tree.adrs ?? []).filter((a) => {
|
|
947
|
+
const t = typeof a?.title === 'string' ? a.title : '';
|
|
948
|
+
return t.startsWith('Deploy target:') || t.startsWith('Deploy deferral:');
|
|
949
|
+
});
|
|
950
|
+
const deployFail = [];
|
|
951
|
+
if (deployAdrs.length === 0) deployFail.push({ id: 'ADR:deploy', why: 'no Deploy target or Deploy deferral ADR found (title-prefix scan)' });
|
|
952
|
+
else if (deployAdrs.length > 1) deployFail.push({ id: 'ADR:deploy', why: `${deployAdrs.length} Deploy ADRs found; expected exactly one` });
|
|
953
|
+
return makeCheck('skeleton:deployAdr', 'tree', 1, deployFail);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Compute the `skeleton:standardsCited` check (engineer, tree-wide).
|
|
958
|
+
* Pure. Every standards pack registered through `rcf define standards`
|
|
959
|
+
* (manifest.standards[].slug) is cited in a REQ `rationale` or an ADR,
|
|
960
|
+
* or satisfied by a concern-ledger entry keyed `standards:<pack>` whose
|
|
961
|
+
* disposition is `applied` or `waived`. Ruling R8 2026-10-05: both
|
|
962
|
+
* `applied` and `waived` satisfy the check (an `applied` entry means the
|
|
963
|
+
* pack was adopted, which is at least as strong as a citation). Ruling
|
|
964
|
+
* R7 2026-10-05: the check runs tree-wide regardless of the D2 scope
|
|
965
|
+
* early-return, through the shared ADR-4138 helper.
|
|
966
|
+
*
|
|
967
|
+
* @param {StageContext} ctx
|
|
968
|
+
* @returns {ReturnType<typeof makeCheck>}
|
|
969
|
+
*/
|
|
970
|
+
function computeStandardsCitedCheck(ctx) {
|
|
971
|
+
const tree = ctx.tree;
|
|
972
|
+
const standardsList = Array.isArray(tree.manifest?.standards)
|
|
973
|
+
? /** @type {any[]} */ (tree.manifest.standards)
|
|
974
|
+
: [];
|
|
975
|
+
/** @type {Array<{ id: string, why: string }>} */
|
|
976
|
+
const standardsFail = [];
|
|
977
|
+
if (standardsList.length > 0) {
|
|
978
|
+
const reqRationales = (tree.requirements ?? [])
|
|
979
|
+
.map((r) => (typeof r?.rationale === 'string' ? r.rationale : ''))
|
|
980
|
+
.join('\n');
|
|
981
|
+
const adrDocs = (tree.adrs ?? []).map((a) => {
|
|
982
|
+
try { return JSON.stringify(a); } catch { return ''; }
|
|
983
|
+
}).join('\n');
|
|
984
|
+
const concernsBody = /** @type {any} */ (ctx.ledgers?.concerns);
|
|
985
|
+
const concernList = Array.isArray(concernsBody?.concerns) ? concernsBody.concerns : [];
|
|
986
|
+
const satisfierSlugs = new Set();
|
|
987
|
+
for (const entry of concernList) {
|
|
988
|
+
if (!entry || typeof entry.concern !== 'string') continue;
|
|
989
|
+
const disposition = typeof entry.disposition === 'string' ? entry.disposition : null;
|
|
990
|
+
// Ruling R8 2026-10-05: both applied and waived satisfy standardsCited.
|
|
991
|
+
if (disposition !== 'applied' && disposition !== 'waived') continue;
|
|
992
|
+
const m = entry.concern.match(/^standards:(.+)$/);
|
|
993
|
+
if (m) satisfierSlugs.add(m[1]);
|
|
994
|
+
}
|
|
995
|
+
for (const pack of standardsList) {
|
|
996
|
+
const slug = typeof pack?.slug === 'string' ? pack.slug : null;
|
|
997
|
+
if (!slug) continue;
|
|
998
|
+
if (satisfierSlugs.has(slug)) continue;
|
|
999
|
+
if (reqRationales.includes(slug)) continue;
|
|
1000
|
+
if (adrDocs.includes(slug)) continue;
|
|
1001
|
+
standardsFail.push({ id: `standards:${slug}`, why: 'uncited, unapplied and unwaived' });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return makeCheck('skeleton:standardsCited', 'tree', Math.max(standardsList.length, 1), standardsFail);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
865
1007
|
/**
|
|
866
1008
|
* D2 -- Requirements and architecture skeleton.
|
|
867
1009
|
*
|
|
@@ -880,7 +1022,25 @@ export function checkD2Skeleton(ctx) {
|
|
|
880
1022
|
const prdInScope = tree.prd && scope.has(tree.prd.prdId ?? '');
|
|
881
1023
|
const tadInScope = tree.tad && scope.has(tree.tad.tadId ?? '');
|
|
882
1024
|
|
|
1025
|
+
// Ruling R7 2026-10-05 (ADR-4138) and R13 2026-10-05 (ADR-4138
|
|
1026
|
+
// extended, PR 292 follow-up): skeleton:standardsCited and
|
|
1027
|
+
// skeleton:deployAdr are tree-wide checks; evaluate them before any
|
|
1028
|
+
// scope early-return so a narrowed D2 scope that excludes every
|
|
1029
|
+
// REQ/PRD/TAD/brief statement still reports an uncited pack or a
|
|
1030
|
+
// missing Deploy ADR.
|
|
1031
|
+
const standardsCheck = computeStandardsCitedCheck(ctx);
|
|
1032
|
+
const deployAdrCheck = computeDeployAdrCheck(ctx);
|
|
1033
|
+
|
|
883
1034
|
if (resolvingStmts.length === 0 && reqInScope.length === 0 && !prdInScope && !tadInScope) {
|
|
1035
|
+
const treeWideFailure = treeWideFailureEnvelope(
|
|
1036
|
+
'D2', gate,
|
|
1037
|
+
[
|
|
1038
|
+
{ name: 'skeleton:standardsCited', check: standardsCheck },
|
|
1039
|
+
{ name: 'skeleton:deployAdr', check: deployAdrCheck },
|
|
1040
|
+
],
|
|
1041
|
+
{ currentTreeHash: ctx.currentTreeHash ?? null, freezeGates: /** @type {any} */ (ctx.freeze?.gates) },
|
|
1042
|
+
);
|
|
1043
|
+
if (treeWideFailure) return treeWideFailure;
|
|
884
1044
|
return notApplicable('D2', gate, 'no resolving brief statements and no REQ / PRD / TAD in scope');
|
|
885
1045
|
}
|
|
886
1046
|
|
|
@@ -952,59 +1112,17 @@ export function checkD2Skeleton(ctx) {
|
|
|
952
1112
|
checks.push(makeCheck('skeleton:tadPersistence', 'tree', persistenceReq ? 2 : 0, tadFail));
|
|
953
1113
|
|
|
954
1114
|
// Check 4: exactly one deploy or deferral ADR tree-wide. Cheap
|
|
955
|
-
// detection by title prefix (proposal §11 assumption).
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
// tree). Every standards pack registered through `rcf define
|
|
967
|
-
// standards` (manifest.standards[].slug) is cited in a REQ
|
|
968
|
-
// `rationale` or an ADR, or waived by a concern-ledger entry keyed
|
|
969
|
-
// `standards:<pack>`. Fail ids `standards:<pack>` with 'uncited and
|
|
970
|
-
// unwaived'. Citation is a case-sensitive substring match of the
|
|
971
|
-
// slug against each REQ.rationale and each ADR document (the ADR
|
|
972
|
-
// may name a pack in its title, context, decision or consequences;
|
|
973
|
-
// the serialised document is scanned once per ADR to cover every
|
|
974
|
-
// field). A waiver entry on the concern ledger reads `concern:
|
|
975
|
-
// 'standards:<pack>'` with disposition 'applied' or 'waived'.
|
|
976
|
-
const standardsList = Array.isArray(tree.manifest?.standards)
|
|
977
|
-
? /** @type {any[]} */ (tree.manifest.standards)
|
|
978
|
-
: [];
|
|
979
|
-
/** @type {Array<{ id: string, why: string }>} */
|
|
980
|
-
const standardsFail = [];
|
|
981
|
-
if (standardsList.length > 0) {
|
|
982
|
-
const reqRationales = (tree.requirements ?? [])
|
|
983
|
-
.map((r) => (typeof r?.rationale === 'string' ? r.rationale : ''))
|
|
984
|
-
.join('\n');
|
|
985
|
-
const adrDocs = (tree.adrs ?? []).map((a) => {
|
|
986
|
-
try { return JSON.stringify(a); } catch { return ''; }
|
|
987
|
-
}).join('\n');
|
|
988
|
-
const concernsBody = /** @type {any} */ (ctx.ledgers?.concerns);
|
|
989
|
-
const concernList = Array.isArray(concernsBody?.concerns) ? concernsBody.concerns : [];
|
|
990
|
-
const waiverSlugs = new Set();
|
|
991
|
-
for (const entry of concernList) {
|
|
992
|
-
if (!entry || typeof entry.concern !== 'string') continue;
|
|
993
|
-
const disposition = typeof entry.disposition === 'string' ? entry.disposition : null;
|
|
994
|
-
if (disposition !== 'applied' && disposition !== 'waived') continue;
|
|
995
|
-
const m = entry.concern.match(/^standards:(.+)$/);
|
|
996
|
-
if (m) waiverSlugs.add(m[1]);
|
|
997
|
-
}
|
|
998
|
-
for (const pack of standardsList) {
|
|
999
|
-
const slug = typeof pack?.slug === 'string' ? pack.slug : null;
|
|
1000
|
-
if (!slug) continue;
|
|
1001
|
-
if (waiverSlugs.has(slug)) continue;
|
|
1002
|
-
if (reqRationales.includes(slug)) continue;
|
|
1003
|
-
if (adrDocs.includes(slug)) continue;
|
|
1004
|
-
standardsFail.push({ id: `standards:${slug}`, why: 'uncited and unwaived' });
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
checks.push(makeCheck('skeleton:standardsCited', 'tree', Math.max(standardsList.length, 1), standardsFail));
|
|
1115
|
+
// detection by title prefix (proposal §11 assumption). Ruling R13
|
|
1116
|
+
// 2026-10-05: tree-wide by definition; computed by
|
|
1117
|
+
// computeDeployAdrCheck so the same check is reused ahead of the
|
|
1118
|
+
// scope early-return (ADR-4138 extended).
|
|
1119
|
+
checks.push(deployAdrCheck);
|
|
1120
|
+
|
|
1121
|
+
// Check 5 (0.30.0 PR 6, ruling R7+R8 2026-10-05): skeleton:standardsCited
|
|
1122
|
+
// (engineer, over tree). Computed by `computeStandardsCitedCheck` so the
|
|
1123
|
+
// same check runs ahead of the scope early-return (R7) and so the R8
|
|
1124
|
+
// behaviour (applied | waived | cited all satisfy) lives in one place.
|
|
1125
|
+
checks.push(standardsCheck);
|
|
1008
1126
|
|
|
1009
1127
|
return foldState('D2', gate, checks, {
|
|
1010
1128
|
currentTreeHash: ctx.currentTreeHash ?? null,
|
|
@@ -1019,6 +1137,98 @@ export function checkD2Skeleton(ctx) {
|
|
|
1019
1137
|
/** Set form of INTERFACE_KINDS for O(1) checks. */
|
|
1020
1138
|
const INTERFACE_KINDS_SET = new Set(INTERFACE_KINDS);
|
|
1021
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Compute the `shapes:draftSettled` check (engineer, tree-wide). Pure.
|
|
1142
|
+
* Ruling R2 2026-10-05 (ADR-4138): a `[draft]` marker on any TAC
|
|
1143
|
+
* purpose, any interface description or any coreEntities[].description
|
|
1144
|
+
* fails the check regardless of the D3 scope's own early-return. The
|
|
1145
|
+
* three id forms are `<tacId>`, `<tacId>:<name>` and `TAD.entity:<name>`.
|
|
1146
|
+
*
|
|
1147
|
+
* @param {StageContext} ctx
|
|
1148
|
+
* @returns {ReturnType<typeof makeCheck>}
|
|
1149
|
+
*/
|
|
1150
|
+
function computeDraftSettledCheck(ctx) {
|
|
1151
|
+
const tree = ctx.tree;
|
|
1152
|
+
const tacs = tree.tacs ?? [];
|
|
1153
|
+
const coreEntities = Array.isArray(tree.tad?.dataArchitecture?.coreEntities)
|
|
1154
|
+
? /** @type {any[]} */ (tree.tad.dataArchitecture.coreEntities)
|
|
1155
|
+
: [];
|
|
1156
|
+
/** @type {Array<{ id: string, why: string }>} */
|
|
1157
|
+
const draftFindings = [];
|
|
1158
|
+
let totalInterfaces = 0;
|
|
1159
|
+
for (const tac of tacs) {
|
|
1160
|
+
if (parseTacPurposeDraft(tac)) {
|
|
1161
|
+
draftFindings.push({ id: tac.tacId, why: 'draft TAC purpose pre-populated at L1, not yet settled' });
|
|
1162
|
+
}
|
|
1163
|
+
for (const iface of tac.interfaces ?? []) {
|
|
1164
|
+
totalInterfaces += 1;
|
|
1165
|
+
if (parseInterfaceDraft(iface)) {
|
|
1166
|
+
draftFindings.push({ id: `${tac.tacId}:${iface?.name ?? '(unnamed)'}`, why: 'draft interface pre-populated at L1, not yet settled' });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
for (const entity of coreEntities) {
|
|
1171
|
+
if (parseCoreEntityDraft(entity)) {
|
|
1172
|
+
draftFindings.push({ id: `TAD.entity:${entity?.name ?? '(unnamed)'}`, why: 'draft core entity pre-populated at L1, not yet settled' });
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const totalDraftHomes = tacs.length + totalInterfaces + coreEntities.length;
|
|
1176
|
+
return makeCheck('shapes:draftSettled', 'tree', Math.max(totalDraftHomes, 1), draftFindings);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Compute the `shapes:entityJoin` check (engineer, tree-wide). Pure.
|
|
1181
|
+
* Ruling R5 2026-10-05 (ADR-4138): every entry in
|
|
1182
|
+
* `TAD.dataArchitecture.coreEntities` must be named by exactly one
|
|
1183
|
+
* `recordShape` across the whole tree (matching on the recordShape's
|
|
1184
|
+
* `name` field or an `entity:` line in its description, with a
|
|
1185
|
+
* `[draft]` prefix stripped before the scan). The check runs ahead of
|
|
1186
|
+
* the D3 scope early-return so a narrowed scope that excludes every
|
|
1187
|
+
* recordShape still reports an unjoined entity.
|
|
1188
|
+
*
|
|
1189
|
+
* @param {StageContext} ctx
|
|
1190
|
+
* @returns {ReturnType<typeof makeCheck>}
|
|
1191
|
+
*/
|
|
1192
|
+
function computeEntityJoinCheck(ctx) {
|
|
1193
|
+
const tree = ctx.tree;
|
|
1194
|
+
const coreEntities = Array.isArray(tree.tad?.dataArchitecture?.coreEntities)
|
|
1195
|
+
? /** @type {any[]} */ (tree.tad.dataArchitecture.coreEntities)
|
|
1196
|
+
: [];
|
|
1197
|
+
/** @type {Array<{ id: string, why: string }>} */
|
|
1198
|
+
const joinFindings = [];
|
|
1199
|
+
const recordShapesByEntity = new Map();
|
|
1200
|
+
for (const tac of tree.tacs ?? []) {
|
|
1201
|
+
for (const iface of tac.interfaces ?? []) {
|
|
1202
|
+
if (iface?.kind !== 'recordShape') continue;
|
|
1203
|
+
const ownerLabel = `${tac.tacId}:${iface?.name ?? '(unnamed)'}`;
|
|
1204
|
+
const names = new Set();
|
|
1205
|
+
if (typeof iface?.name === 'string') names.add(iface.name);
|
|
1206
|
+
const desc = typeof iface?.description === 'string' ? stripDraftPrefix(iface.description) : '';
|
|
1207
|
+
const entityRe = /(^|\n)\s*(?:[-*]\s*)?entity\s*[:=]\s*["'`]?([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1208
|
+
let em;
|
|
1209
|
+
while ((em = entityRe.exec(desc)) !== null) {
|
|
1210
|
+
if (em[2]) names.add(em[2]);
|
|
1211
|
+
}
|
|
1212
|
+
for (const name of names) {
|
|
1213
|
+
const owners = recordShapesByEntity.get(name) ?? [];
|
|
1214
|
+
owners.push(ownerLabel);
|
|
1215
|
+
recordShapesByEntity.set(name, owners);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (const entity of coreEntities) {
|
|
1220
|
+
const name = typeof entity?.name === 'string' ? entity.name : null;
|
|
1221
|
+
if (!name) continue;
|
|
1222
|
+
const owners = recordShapesByEntity.get(name) ?? [];
|
|
1223
|
+
if (owners.length === 0) {
|
|
1224
|
+
joinFindings.push({ id: `TAD.entity:${name}`, why: 'no record shape' });
|
|
1225
|
+
} else if (owners.length > 1) {
|
|
1226
|
+
joinFindings.push({ id: `TAD.entity:${name}`, why: `${owners.length} record shapes` });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return makeCheck('shapes:entityJoin', 'tree', coreEntities.length, joinFindings);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1022
1232
|
/**
|
|
1023
1233
|
* D3 -- Interface contracts and shapes. 0.29.0 runs the cheap
|
|
1024
1234
|
* presence, closed-vocabulary and draft-marker checks
|
|
@@ -1055,10 +1265,30 @@ export function checkD3Shapes(ctx) {
|
|
|
1055
1265
|
const coreEntities = Array.isArray(tree.tad?.dataArchitecture?.coreEntities)
|
|
1056
1266
|
? /** @type {any[]} */ (tree.tad.dataArchitecture.coreEntities)
|
|
1057
1267
|
: [];
|
|
1058
|
-
const draftEntities = coreEntities.filter(parseCoreEntityDraft);
|
|
1059
1268
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1269
|
+
// Ruling R2+R5 2026-10-05 (ADR-4138): shapes:draftSettled and
|
|
1270
|
+
// shapes:entityJoin are tree-wide checks; evaluate them before any
|
|
1271
|
+
// scope early-return so a narrowed D3 scope that excludes every TAC
|
|
1272
|
+
// and shaped REQ still reports a [draft] anywhere in the tree and an
|
|
1273
|
+
// unjoined core entity.
|
|
1274
|
+
const draftCheck = computeDraftSettledCheck(ctx);
|
|
1275
|
+
const entityJoinCheck = computeEntityJoinCheck(ctx);
|
|
1276
|
+
|
|
1277
|
+
if (tacsInScope.length === 0 && shapedReqInScope.length === 0) {
|
|
1278
|
+
const treeWideFailure = treeWideFailureEnvelope(
|
|
1279
|
+
'D3', gate,
|
|
1280
|
+
[
|
|
1281
|
+
{ name: 'shapes:draftSettled', check: draftCheck },
|
|
1282
|
+
{ name: 'shapes:entityJoin', check: entityJoinCheck },
|
|
1283
|
+
],
|
|
1284
|
+
{ currentTreeHash: ctx.currentTreeHash ?? null, freezeGates: /** @type {any} */ (ctx.freeze?.gates) },
|
|
1285
|
+
);
|
|
1286
|
+
if (treeWideFailure) return treeWideFailure;
|
|
1287
|
+
// Ruling R2 2026-10-05: `[draft]` is tree-wide (not a D3 scope
|
|
1288
|
+
// input), so the pre-R2 reason trailer "and no [draft] core entity
|
|
1289
|
+
// in scope" was stale -- the applicability condition is just the
|
|
1290
|
+
// two local-scope sets.
|
|
1291
|
+
return notApplicable('D3', gate, 'no TAC and no shaped REQ (httpApi/persistence/auth) in scope');
|
|
1062
1292
|
}
|
|
1063
1293
|
|
|
1064
1294
|
const checks = [];
|
|
@@ -1072,40 +1302,26 @@ export function checkD3Shapes(ctx) {
|
|
|
1072
1302
|
noIfaceTacs.map((t) => ({ id: t.tacId, why: 'no interfaces authored' })),
|
|
1073
1303
|
));
|
|
1074
1304
|
|
|
1075
|
-
// Check 2: every interface kind is in the closed vocabulary
|
|
1076
|
-
//
|
|
1077
|
-
// carries the `[draft]` marker. A draft shape is entry material
|
|
1078
|
-
// the agent pre-populates after L1; the engineer removes the
|
|
1079
|
-
// marker when the shape is settled. `tacHasInterface` and
|
|
1080
|
-
// `kindVocabulary` apply equally to drafts.
|
|
1305
|
+
// Check 2: every interface kind is in the closed vocabulary (over
|
|
1306
|
+
// delta; drafts included).
|
|
1081
1307
|
let totalInterfaces = 0;
|
|
1082
1308
|
const badKinds = [];
|
|
1083
|
-
const draftFindings = [];
|
|
1084
1309
|
for (const tac of tacsInScope) {
|
|
1085
|
-
if (parseTacPurposeDraft(tac)) {
|
|
1086
|
-
draftFindings.push({ id: tac.tacId, why: 'draft TAC purpose pre-populated at L1, not yet settled' });
|
|
1087
|
-
}
|
|
1088
1310
|
for (const iface of tac.interfaces ?? []) {
|
|
1089
1311
|
totalInterfaces += 1;
|
|
1090
1312
|
const kind = iface?.kind;
|
|
1091
1313
|
if (typeof kind !== 'string' || !INTERFACE_KINDS_SET.has(kind)) {
|
|
1092
1314
|
badKinds.push({ id: `${tac.tacId}:${iface?.name ?? '(unnamed)'}`, why: `unknown interface kind ${JSON.stringify(kind)}` });
|
|
1093
1315
|
}
|
|
1094
|
-
if (parseInterfaceDraft(iface)) {
|
|
1095
|
-
draftFindings.push({ id: `${tac.tacId}:${iface?.name ?? '(unnamed)'}`, why: 'draft interface pre-populated at L1, not yet settled' });
|
|
1096
|
-
}
|
|
1097
1316
|
}
|
|
1098
1317
|
}
|
|
1099
|
-
for (const entity of draftEntities) {
|
|
1100
|
-
draftFindings.push({ id: `TAD.entity:${entity?.name ?? '(unnamed)'}`, why: 'draft core entity pre-populated at L1, not yet settled' });
|
|
1101
|
-
}
|
|
1102
|
-
// Total for the draft check is every draft home that could carry
|
|
1103
|
-
// the marker on an in-scope TAC plus every core entity (tree-wide):
|
|
1104
|
-
// the TAC itself (its purpose) + every interface on it, plus the
|
|
1105
|
-
// coreEntities list.
|
|
1106
|
-
const totalDraftHomes = tacsInScope.length + totalInterfaces + coreEntities.length;
|
|
1107
1318
|
checks.push(makeCheck('shapes:kindVocabulary', 'delta', totalInterfaces, badKinds));
|
|
1108
|
-
|
|
1319
|
+
|
|
1320
|
+
// Check 3: shapes:draftSettled. Ruling R2 2026-10-05: tree-wide (a
|
|
1321
|
+
// [draft] anywhere is unfinished work). Computed by
|
|
1322
|
+
// computeDraftSettledCheck so the same check is reused ahead of the
|
|
1323
|
+
// scope early-return.
|
|
1324
|
+
checks.push(draftCheck);
|
|
1109
1325
|
|
|
1110
1326
|
// ADR-4131 (0.30.0 PR 5): three D3 bite checks follow.
|
|
1111
1327
|
// Check 4: shapes:templateMarkers (engineer, over delta). For every
|
|
@@ -1132,46 +1348,10 @@ export function checkD3Shapes(ctx) {
|
|
|
1132
1348
|
}
|
|
1133
1349
|
checks.push(makeCheck('shapes:templateMarkers', 'delta', totalInterfaces, templateFindings));
|
|
1134
1350
|
|
|
1135
|
-
// Check 5: shapes:entityJoin
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
// in its description. A `[draft]` prefix is stripped before the
|
|
1140
|
-
// entity-line scan so a draft recordShape still joins.
|
|
1141
|
-
/** @type {Array<{ id: string, why: string }>} */
|
|
1142
|
-
const joinFindings = [];
|
|
1143
|
-
const treeTacs = tree.tacs ?? [];
|
|
1144
|
-
const recordShapesByEntity = new Map();
|
|
1145
|
-
for (const tac of treeTacs) {
|
|
1146
|
-
for (const iface of tac.interfaces ?? []) {
|
|
1147
|
-
if (iface?.kind !== 'recordShape') continue;
|
|
1148
|
-
const ownerLabel = `${tac.tacId}:${iface?.name ?? '(unnamed)'}`;
|
|
1149
|
-
const names = new Set();
|
|
1150
|
-
if (typeof iface?.name === 'string') names.add(iface.name);
|
|
1151
|
-
const desc = typeof iface?.description === 'string' ? stripDraftPrefix(iface.description) : '';
|
|
1152
|
-
const entityRe = /(^|\n)\s*(?:[-*]\s*)?entity\s*[:=]\s*["'`]?([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1153
|
-
let em;
|
|
1154
|
-
while ((em = entityRe.exec(desc)) !== null) {
|
|
1155
|
-
if (em[2]) names.add(em[2]);
|
|
1156
|
-
}
|
|
1157
|
-
for (const name of names) {
|
|
1158
|
-
const owners = recordShapesByEntity.get(name) ?? [];
|
|
1159
|
-
owners.push(ownerLabel);
|
|
1160
|
-
recordShapesByEntity.set(name, owners);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
for (const entity of coreEntities) {
|
|
1165
|
-
const name = typeof entity?.name === 'string' ? entity.name : null;
|
|
1166
|
-
if (!name) continue;
|
|
1167
|
-
const owners = recordShapesByEntity.get(name) ?? [];
|
|
1168
|
-
if (owners.length === 0) {
|
|
1169
|
-
joinFindings.push({ id: `TAD.entity:${name}`, why: 'no record shape' });
|
|
1170
|
-
} else if (owners.length > 1) {
|
|
1171
|
-
joinFindings.push({ id: `TAD.entity:${name}`, why: `${owners.length} record shapes` });
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
checks.push(makeCheck('shapes:entityJoin', 'tree', coreEntities.length, joinFindings));
|
|
1351
|
+
// Check 5: shapes:entityJoin. Ruling R5 2026-10-05: tree-wide by
|
|
1352
|
+
// definition. Computed by computeEntityJoinCheck so the same check
|
|
1353
|
+
// is reused ahead of the scope early-return (ADR-4138).
|
|
1354
|
+
checks.push(entityJoinCheck);
|
|
1175
1355
|
|
|
1176
1356
|
// Check 6: shapes:pathsResolve (engineer, over delta). Any path:
|
|
1177
1357
|
// token on an in-scope interface description must either resolve
|
|
@@ -1363,27 +1543,17 @@ export function checkD4Stories(ctx) {
|
|
|
1363
1543
|
// ---------------------------------------------------------------------------
|
|
1364
1544
|
|
|
1365
1545
|
/**
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1546
|
+
* Compute the `crosscut:securityArchitecture` check (engineer,
|
|
1547
|
+
* tree-wide). Pure. Ruling R13 2026-10-05 (ADR-4138 extended): if the
|
|
1548
|
+
* tree carries any REQ with shape `auth` or `httpApi`, TAD.securityArchitecture
|
|
1549
|
+
* must be non-empty. Runs ahead of the D5 scope early-return so a
|
|
1550
|
+
* narrowed scope that excludes the auth REQ still reports the finding.
|
|
1371
1551
|
*
|
|
1372
1552
|
* @param {StageContext} ctx
|
|
1553
|
+
* @returns {ReturnType<typeof makeCheck>}
|
|
1373
1554
|
*/
|
|
1374
|
-
|
|
1375
|
-
const gate = STAGE_GATES.D5;
|
|
1376
|
-
const scope = asSet(ctx.scope);
|
|
1555
|
+
function computeSecurityArchitectureCheck(ctx) {
|
|
1377
1556
|
const tree = ctx.tree;
|
|
1378
|
-
const reqInScope = (tree.requirements ?? []).filter((r) => scope.has(r.reqId));
|
|
1379
|
-
|
|
1380
|
-
if (reqInScope.length === 0) {
|
|
1381
|
-
return notApplicable('D5', gate, 'no REQ in scope');
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
const checks = [];
|
|
1385
|
-
|
|
1386
|
-
// Check 1: TAD.securityArchitecture non-empty when any REQ carries shape 'auth' or 'httpApi'.
|
|
1387
1557
|
const authApi = (tree.requirements ?? []).some((r) => {
|
|
1388
1558
|
const s = r.shapeClassification?.shapes ?? [];
|
|
1389
1559
|
return s.includes('auth') || s.includes('httpApi');
|
|
@@ -1396,11 +1566,22 @@ export function checkD5Crosscut(ctx) {
|
|
|
1396
1566
|
secFail.push({ id: 'TAD.securityArchitecture', why: 'empty while auth or httpApi REQ exists' });
|
|
1397
1567
|
}
|
|
1398
1568
|
}
|
|
1399
|
-
|
|
1569
|
+
return makeCheck('crosscut:securityArchitecture', 'tree', authApi ? 1 : 0, secFail);
|
|
1570
|
+
}
|
|
1400
1571
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1572
|
+
/**
|
|
1573
|
+
* Compute the `crosscut:operationalConcerns` check (engineer,
|
|
1574
|
+
* tree-wide). Pure. Ruling R13 2026-10-05 (ADR-4138 extended): if any
|
|
1575
|
+
* AC description tree-wide carries the `[deployed]` marker, then
|
|
1576
|
+
* TAD.operationalConcerns must be non-empty. Runs ahead of the D5
|
|
1577
|
+
* scope early-return.
|
|
1578
|
+
*
|
|
1579
|
+
* @param {StageContext} ctx
|
|
1580
|
+
* @returns {ReturnType<typeof makeCheck>}
|
|
1581
|
+
*/
|
|
1582
|
+
function computeOperationalConcernsCheck(ctx) {
|
|
1583
|
+
const tree = ctx.tree;
|
|
1584
|
+
const tad = /** @type {any} */ (tree.tad ?? {});
|
|
1404
1585
|
const hasDeployedAc = (tree.userStories ?? []).some((us) => (us.acceptanceCriteria ?? []).some((ac) => typeof ac?.description === 'string' && ac.description.includes('[deployed]')));
|
|
1405
1586
|
const opFail = [];
|
|
1406
1587
|
if (hasDeployedAc) {
|
|
@@ -1409,7 +1590,59 @@ export function checkD5Crosscut(ctx) {
|
|
|
1409
1590
|
opFail.push({ id: 'TAD.operationalConcerns', why: 'empty while deployed-scope AC exists' });
|
|
1410
1591
|
}
|
|
1411
1592
|
}
|
|
1412
|
-
|
|
1593
|
+
return makeCheck('crosscut:operationalConcerns', 'tree', hasDeployedAc ? 1 : 0, opFail);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* D5 -- Cross-cutting weave. 0.29.0 runs cheap presence checks.
|
|
1598
|
+
*
|
|
1599
|
+
* SEAM 0.30: full concern catalogue with per-shape applicability;
|
|
1600
|
+
* per-(concern, REQ) pair enumeration; TAD securityArchitecture and
|
|
1601
|
+
* operationalConcerns structure checks beyond presence.
|
|
1602
|
+
*
|
|
1603
|
+
* @param {StageContext} ctx
|
|
1604
|
+
*/
|
|
1605
|
+
export function checkD5Crosscut(ctx) {
|
|
1606
|
+
const gate = STAGE_GATES.D5;
|
|
1607
|
+
const scope = asSet(ctx.scope);
|
|
1608
|
+
const tree = ctx.tree;
|
|
1609
|
+
const reqInScope = (tree.requirements ?? []).filter((r) => scope.has(r.reqId));
|
|
1610
|
+
|
|
1611
|
+
// Ruling R13 2026-10-05 (ADR-4138 extended, PR 292 follow-up):
|
|
1612
|
+
// crosscut:securityArchitecture and crosscut:operationalConcerns are
|
|
1613
|
+
// tree-wide checks; evaluate them before any scope early-return so a
|
|
1614
|
+
// narrowed D5 scope that excludes the triggering REQ or AC still
|
|
1615
|
+
// reports an empty TAD.securityArchitecture / TAD.operationalConcerns.
|
|
1616
|
+
const securityCheck = computeSecurityArchitectureCheck(ctx);
|
|
1617
|
+
const operationalCheck = computeOperationalConcernsCheck(ctx);
|
|
1618
|
+
|
|
1619
|
+
if (reqInScope.length === 0) {
|
|
1620
|
+
const treeWideFailure = treeWideFailureEnvelope(
|
|
1621
|
+
'D5', gate,
|
|
1622
|
+
[
|
|
1623
|
+
{ name: 'crosscut:securityArchitecture', check: securityCheck },
|
|
1624
|
+
{ name: 'crosscut:operationalConcerns', check: operationalCheck },
|
|
1625
|
+
],
|
|
1626
|
+
{ currentTreeHash: ctx.currentTreeHash ?? null, freezeGates: /** @type {any} */ (ctx.freeze?.gates) },
|
|
1627
|
+
);
|
|
1628
|
+
if (treeWideFailure) return treeWideFailure;
|
|
1629
|
+
return notApplicable('D5', gate, 'no REQ in scope');
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const checks = [];
|
|
1633
|
+
|
|
1634
|
+
// Check 1: TAD.securityArchitecture non-empty when any REQ carries
|
|
1635
|
+
// shape 'auth' or 'httpApi'. Ruling R13 2026-10-05: tree-wide;
|
|
1636
|
+
// computed by computeSecurityArchitectureCheck so the same check is
|
|
1637
|
+
// reused ahead of the scope early-return (ADR-4138 extended).
|
|
1638
|
+
checks.push(securityCheck);
|
|
1639
|
+
|
|
1640
|
+
// Check 2: TAD.operationalConcerns non-empty when any AC description
|
|
1641
|
+
// carries the '[deployed]' marker (a cheap presence signal that
|
|
1642
|
+
// proposal §3.2 D5 lists as 'deployed-scope AC'). Ruling R13
|
|
1643
|
+
// 2026-10-05: tree-wide; computed by computeOperationalConcernsCheck
|
|
1644
|
+
// so the same check is reused ahead of the scope early-return.
|
|
1645
|
+
checks.push(operationalCheck);
|
|
1413
1646
|
|
|
1414
1647
|
// Check 3: no open concern-ledger entry on a REQ in scope.
|
|
1415
1648
|
const concerns = /** @type {any} */ (ctx.ledgers?.concerns);
|