@deftai/directive-core 0.104.0 → 0.105.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/dist/authz/classify.js +526 -42
- package/dist/check/cached-orchestrator.d.ts +1 -1
- package/dist/check/cached-orchestrator.js +2 -2
- package/dist/check/named-cause.d.ts +1 -1
- package/dist/check/named-cause.js +33 -3
- package/dist/codebase/default-extractor.d.ts +5 -0
- package/dist/codebase/default-extractor.js +7 -15
- package/dist/deposit/copy-tree.js +60 -0
- package/dist/deposit/python-free.js +26 -18
- package/dist/doctor/checks.d.ts +8 -0
- package/dist/doctor/checks.js +37 -25
- package/dist/doctor/doctor-state.d.ts +4 -0
- package/dist/doctor/doctor-state.js +7 -1
- package/dist/doctor/main.js +9 -19
- package/dist/doctor/types.d.ts +9 -0
- package/dist/fs/contained-write.d.ts +115 -1
- package/dist/fs/contained-write.js +241 -5
- package/dist/fs/mutation-ledger.d.ts +54 -0
- package/dist/fs/mutation-ledger.js +133 -0
- package/dist/fs/non-product-dirs.d.ts +79 -0
- package/dist/fs/non-product-dirs.js +96 -0
- package/dist/hooks/dispatcher.d.ts +3 -1
- package/dist/hooks/dispatcher.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.js +17 -15
- package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
- package/dist/init-deposit/core-guard-pin-content.js +393 -0
- package/dist/init-deposit/gitignore.js +9 -0
- package/dist/init-deposit/hygiene.d.ts +47 -11
- package/dist/init-deposit/hygiene.js +172 -37
- package/dist/init-deposit/legacy-detect.js +6 -2
- package/dist/init-deposit/refresh.d.ts +21 -1
- package/dist/init-deposit/refresh.js +106 -9
- package/dist/init-deposit/scaffold.js +28 -279
- package/dist/init-deposit/slash-deposit.js +13 -7
- package/dist/init-deposit/xbrief-projections.d.ts +8 -1
- package/dist/init-deposit/xbrief-projections.js +24 -11
- package/dist/intake/clause-derivation.d.ts +38 -1
- package/dist/intake/clause-derivation.js +211 -9
- package/dist/intake/issue-ingest.d.ts +3 -1
- package/dist/intake/issue-ingest.js +35 -6
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
- package/dist/literal-acceptance/capture.d.ts +10 -0
- package/dist/literal-acceptance/capture.js +224 -75
- package/dist/literal-acceptance/evaluate.d.ts +29 -4
- package/dist/literal-acceptance/evaluate.js +70 -2
- package/dist/literal-acceptance/index.d.ts +3 -3
- package/dist/literal-acceptance/index.js +3 -3
- package/dist/literal-acceptance/safety.d.ts +41 -0
- package/dist/literal-acceptance/safety.js +126 -2
- package/dist/literal-acceptance/types.d.ts +8 -0
- package/dist/orphan-active/evaluate.d.ts +7 -1
- package/dist/orphan-active/evaluate.js +102 -23
- package/dist/platform/platform-capabilities.js +2 -3
- package/dist/policy/base-branch.d.ts +37 -0
- package/dist/policy/base-branch.js +114 -0
- package/dist/policy/branch-sync.d.ts +80 -0
- package/dist/policy/branch-sync.js +211 -0
- package/dist/policy/ceremony-dial-escalation.js +1 -0
- package/dist/policy/ceremony-dial.js +4 -14
- package/dist/policy/delivery-branch.d.ts +5 -3
- package/dist/policy/delivery-branch.js +7 -3
- package/dist/policy/file-size-thresholds.d.ts +24 -0
- package/dist/policy/file-size-thresholds.js +23 -0
- package/dist/policy/forge-outage-retry.d.ts +47 -0
- package/dist/policy/forge-outage-retry.js +157 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.js +66 -7
- package/dist/policy/project-invariants.d.ts +50 -0
- package/dist/policy/project-invariants.js +205 -0
- package/dist/policy/sync-default.d.ts +129 -0
- package/dist/policy/sync-default.js +452 -0
- package/dist/policy/sync-max-files.d.ts +93 -0
- package/dist/policy/sync-max-files.js +192 -0
- package/dist/pr-monitor/absent-required.d.ts +11 -0
- package/dist/pr-monitor/absent-required.js +61 -0
- package/dist/pr-monitor/constants.d.ts +7 -0
- package/dist/pr-monitor/constants.js +7 -0
- package/dist/pr-monitor/index.d.ts +2 -1
- package/dist/pr-monitor/index.js +2 -1
- package/dist/pr-monitor/main.js +4 -0
- package/dist/pr-monitor/monitor.js +24 -1
- package/dist/pr-wait-mergeable/classify.js +6 -0
- package/dist/preflight/evaluate.d.ts +4 -0
- package/dist/preflight/evaluate.js +35 -1
- package/dist/preflight/index.d.ts +4 -0
- package/dist/preflight/index.js +2 -0
- package/dist/preflight/intended-placement.d.ts +62 -0
- package/dist/preflight/intended-placement.js +275 -0
- package/dist/preflight/project-invariants-gate.d.ts +29 -0
- package/dist/preflight/project-invariants-gate.js +82 -0
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
- package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
- package/dist/product-first-done-gate/acceptance.d.ts +7 -4
- package/dist/product-first-done-gate/acceptance.js +79 -8
- package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
- package/dist/product-first-done-gate/empty-resolution.js +12 -1
- package/dist/product-first-done-gate/evaluate.d.ts +12 -0
- package/dist/product-first-done-gate/evaluate.js +254 -31
- package/dist/product-first-done-gate/index.d.ts +2 -0
- package/dist/product-first-done-gate/index.js +1 -0
- package/dist/release/build-dist.d.ts +101 -0
- package/dist/release/build-dist.js +428 -46
- package/dist/run-summary/emit.d.ts +16 -10
- package/dist/run-summary/emit.js +26 -15
- package/dist/run-summary/index.d.ts +3 -2
- package/dist/run-summary/index.js +2 -1
- package/dist/run-summary/session-id.d.ts +21 -0
- package/dist/run-summary/session-id.js +92 -0
- package/dist/run-summary/types.d.ts +29 -4
- package/dist/run-summary/types.js +1 -0
- package/dist/scope/acceptance-evidence.d.ts +19 -7
- package/dist/scope/acceptance-evidence.js +38 -11
- package/dist/scope/delivery-evidence.d.ts +3 -3
- package/dist/scope/delivery-evidence.js +22 -27
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/parent-lineage.js +7 -6
- package/dist/scope/project-invariant-coverage.d.ts +54 -0
- package/dist/scope/project-invariant-coverage.js +322 -0
- package/dist/scope/transition.js +1 -1
- package/dist/scope-provenance/compare-intent.d.ts +27 -0
- package/dist/scope-provenance/compare-intent.js +233 -0
- package/dist/scope-provenance/digest.d.ts +18 -1
- package/dist/scope-provenance/digest.js +16 -11
- package/dist/scope-provenance/evaluate.d.ts +5 -1
- package/dist/scope-provenance/evaluate.js +64 -4
- package/dist/scope-provenance/extract-intent.d.ts +43 -0
- package/dist/scope-provenance/extract-intent.js +262 -0
- package/dist/scope-provenance/index.d.ts +7 -1
- package/dist/scope-provenance/index.js +7 -1
- package/dist/scope-provenance/intent-digest.d.ts +12 -0
- package/dist/scope-provenance/intent-digest.js +33 -0
- package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
- package/dist/scope-provenance/intent-evaluate.js +149 -0
- package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
- package/dist/scope-provenance/json-tokenizer.js +225 -0
- package/dist/scope-provenance/known-machine.d.ts +38 -0
- package/dist/scope-provenance/known-machine.js +157 -0
- package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
- package/dist/scope-provenance/mint-artifacts.js +425 -0
- package/dist/session/ac-pass-banking.d.ts +4 -36
- package/dist/session/ac-pass-banking.js +30 -77
- package/dist/session/ac-pass-reuse.d.ts +53 -0
- package/dist/session/ac-pass-reuse.js +95 -0
- package/dist/session/git.js +19 -4
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +4 -0
- package/dist/session/occupancy-stress-worker.d.ts +7 -0
- package/dist/session/occupancy-stress-worker.js +36 -0
- package/dist/session/occupancy.d.ts +97 -0
- package/dist/session/occupancy.js +422 -0
- package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
- package/dist/session/product-state-hash.d.ts +26 -0
- package/dist/session/product-state-hash.js +683 -0
- package/dist/session/session-ready.d.ts +2 -0
- package/dist/session/session-ready.js +52 -30
- package/dist/session/session-start.d.ts +7 -0
- package/dist/session/session-start.js +71 -7
- package/dist/session/verify-ac-session-cache.d.ts +42 -0
- package/dist/session/verify-ac-session-cache.js +126 -0
- package/dist/session/verify-session-ritual.d.ts +5 -0
- package/dist/session/verify-session-ritual.js +9 -0
- package/dist/slash/openclaw-deposit.js +13 -9
- package/dist/slice/constants.d.ts +2 -2
- package/dist/slice/constants.js +2 -2
- package/dist/slice/existing.js +9 -3
- package/dist/slice/lock.d.ts +16 -1
- package/dist/slice/lock.js +137 -11
- package/dist/story-ready/evaluate.d.ts +2 -0
- package/dist/story-ready/evaluate.js +13 -0
- package/dist/swarm/complete-cohort.d.ts +3 -0
- package/dist/swarm/complete-cohort.js +59 -11
- package/dist/swarm/finalize-cohort.js +33 -2
- package/dist/swarm/launch.d.ts +22 -0
- package/dist/swarm/launch.js +155 -0
- package/dist/swarm/routing-set-cli.d.ts +11 -1
- package/dist/swarm/routing-set-cli.js +13 -3
- package/dist/swarm/routing-verify.d.ts +12 -0
- package/dist/swarm/routing-verify.js +59 -20
- package/dist/swarm/routing.d.ts +14 -0
- package/dist/swarm/routing.js +53 -3
- package/dist/telemetry-coverage/fake-trial.js +10 -0
- package/dist/telemetry-coverage/kinds.js +2 -0
- package/dist/triage/actions/index.js +4 -1
- package/dist/ts-check-lane/index.d.ts +3 -0
- package/dist/ts-check-lane/index.js +2 -0
- package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
- package/dist/ts-check-lane/progress-reporter.js +38 -0
- package/dist/ts-check-lane/progress.d.ts +32 -0
- package/dist/ts-check-lane/progress.js +57 -0
- package/dist/ts-check-lane/run-lane.d.ts +3 -1
- package/dist/ts-check-lane/run-lane.js +8 -6
- package/dist/validate-content/validate-links.d.ts +8 -0
- package/dist/validate-content/validate-links.js +9 -15
- package/dist/value/feedback-file.js +12 -2
- package/dist/vbrief-build/index.d.ts +1 -0
- package/dist/vbrief-build/index.js +1 -0
- package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
- package/dist/vbrief-build/project-invariants-io.js +41 -0
- package/dist/vbrief-reconcile/graph.js +18 -1
- package/dist/vbrief-reconcile/index.d.ts +1 -1
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +13 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.js +261 -26
- package/dist/vbrief-validate/index.d.ts +1 -1
- package/dist/vbrief-validate/index.js +1 -1
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +68 -0
- package/dist/verify-ac/evaluate.d.ts +7 -1
- package/dist/verify-ac/evaluate.js +34 -26
- package/dist/verify-ac/flag.d.ts +4 -0
- package/dist/verify-ac/flag.js +16 -0
- package/dist/verify-ac/index.d.ts +2 -2
- package/dist/verify-ac/index.js +2 -2
- package/dist/verify-source/contained-writes.js +2 -0
- package/dist/verify-source/verify-stubs.d.ts +15 -1
- package/dist/verify-source/verify-stubs.js +20 -16
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
- package/dist/xbrief-migrate/migrate-project.js +2 -5
- package/package.json +3 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-invariant coverage + applicability (#3425 Story A).
|
|
3
|
+
*
|
|
4
|
+
* Reuses the #3238 coverage_map parser and side-field rules. `split` is
|
|
5
|
+
* excluded at project level. Optional `not_applicable` requires a reason.
|
|
6
|
+
* Applicability is a pure intersection of contract surface × file_scope.
|
|
7
|
+
*/
|
|
8
|
+
import type { ProjectInvariant } from "../policy/project-invariants.js";
|
|
9
|
+
export declare const PROJECT_INVARIANT_DISPOSITIONS: readonly ["covered", "deferred", "behavioral_delta", "not_applicable"];
|
|
10
|
+
export type ProjectInvariantDisposition = (typeof PROJECT_INVARIANT_DISPOSITIONS)[number];
|
|
11
|
+
export interface ApplicableInvariant {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly reason: "path" | "module" | "unresolved-module";
|
|
14
|
+
}
|
|
15
|
+
export interface ProjectInvariantCoverageResult {
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly applicableIds: readonly string[];
|
|
18
|
+
readonly missingIds: readonly string[];
|
|
19
|
+
readonly errors: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
export type GlobDepth = "exact" | "one" | "subtree";
|
|
22
|
+
export interface ClassifiedGlob {
|
|
23
|
+
readonly prefix: string;
|
|
24
|
+
readonly depth: GlobDepth;
|
|
25
|
+
readonly leadingDoubleStar: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify a path/glob without regular expressions (CodeQL js/polynomial-redos).
|
|
29
|
+
* Unstarred paths are directory prefixes (file_scope semantics).
|
|
30
|
+
*/
|
|
31
|
+
export declare function classifyGlob(glob: string): ClassifiedGlob;
|
|
32
|
+
/** Strip glob tails so prefix comparison stays path-shaped. */
|
|
33
|
+
export declare function normalizeGlobAsPath(glob: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* True when two path/glob strings can name a shared tree.
|
|
36
|
+
* `/*` is one segment; `/**` / unstarred file_scope paths are subtrees.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pathGlobsIntersect(left: string, right: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Invariants whose contract surface intersects `fileScope`.
|
|
41
|
+
* Empty file_scope → empty intersection (no disposition required).
|
|
42
|
+
* Unresolved module ids (no pathGlobs mapping) are treated as applicable.
|
|
43
|
+
*/
|
|
44
|
+
export declare function applicableProjectInvariants(invariants: readonly ProjectInvariant[], fileScope: readonly string[], modulePathGlobs?: Readonly<Record<string, readonly string[]>>): ApplicableInvariant[];
|
|
45
|
+
/**
|
|
46
|
+
* Completeness of coverage_map dispositions for applicable project invariant IDs.
|
|
47
|
+
* Extra coverage_map keys (e.g. parent requirement IDs) are ignored.
|
|
48
|
+
* Empty applicable set is a no-op success.
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateProjectInvariantCoverage(opts: {
|
|
51
|
+
applicableIds: readonly string[];
|
|
52
|
+
draft: unknown;
|
|
53
|
+
}): ProjectInvariantCoverageResult;
|
|
54
|
+
//# sourceMappingURL=project-invariant-coverage.d.ts.map
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-invariant coverage + applicability (#3425 Story A).
|
|
3
|
+
*
|
|
4
|
+
* Reuses the #3238 coverage_map parser and side-field rules. `split` is
|
|
5
|
+
* excluded at project level. Optional `not_applicable` requires a reason.
|
|
6
|
+
* Applicability is a pure intersection of contract surface × file_scope.
|
|
7
|
+
*/
|
|
8
|
+
import { matchPath, normalizePath } from "../orchestration/pathspec.js";
|
|
9
|
+
import { parseBehavioralDeltas, parseCoverageMap } from "./coverage-map.js";
|
|
10
|
+
export const PROJECT_INVARIANT_DISPOSITIONS = [
|
|
11
|
+
"covered",
|
|
12
|
+
"deferred",
|
|
13
|
+
"behavioral_delta",
|
|
14
|
+
"not_applicable",
|
|
15
|
+
];
|
|
16
|
+
function asRecord(value) {
|
|
17
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function isNonEmptyString(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Classify a path/glob without regular expressions (CodeQL js/polynomial-redos).
|
|
27
|
+
* Unstarred paths are directory prefixes (file_scope semantics).
|
|
28
|
+
*/
|
|
29
|
+
export function classifyGlob(glob) {
|
|
30
|
+
let p = normalizePath(glob).trim();
|
|
31
|
+
if (p.endsWith("/"))
|
|
32
|
+
p = p.slice(0, -1);
|
|
33
|
+
if (p === "**") {
|
|
34
|
+
return { prefix: "", depth: "subtree", leadingDoubleStar: true };
|
|
35
|
+
}
|
|
36
|
+
if (p.startsWith("**/")) {
|
|
37
|
+
let rest = p.slice(3);
|
|
38
|
+
if (rest.endsWith("/*"))
|
|
39
|
+
rest = rest.slice(0, -2);
|
|
40
|
+
const inner = rest.indexOf("/**");
|
|
41
|
+
if (inner >= 0)
|
|
42
|
+
rest = rest.slice(0, inner);
|
|
43
|
+
return { prefix: rest, depth: "subtree", leadingDoubleStar: true };
|
|
44
|
+
}
|
|
45
|
+
const starStar = p.indexOf("/**");
|
|
46
|
+
if (starStar >= 0) {
|
|
47
|
+
return { prefix: p.slice(0, starStar), depth: "subtree", leadingDoubleStar: false };
|
|
48
|
+
}
|
|
49
|
+
if (p.endsWith("/*")) {
|
|
50
|
+
return { prefix: p.slice(0, -2), depth: "one", leadingDoubleStar: false };
|
|
51
|
+
}
|
|
52
|
+
if (p.includes("*") || p.includes("?")) {
|
|
53
|
+
const slashStar = p.indexOf("/*");
|
|
54
|
+
if (slashStar >= 0) {
|
|
55
|
+
return { prefix: p.slice(0, slashStar), depth: "subtree", leadingDoubleStar: false };
|
|
56
|
+
}
|
|
57
|
+
return { prefix: p, depth: "subtree", leadingDoubleStar: false };
|
|
58
|
+
}
|
|
59
|
+
return { prefix: p, depth: "subtree", leadingDoubleStar: false };
|
|
60
|
+
}
|
|
61
|
+
/** Strip glob tails so prefix comparison stays path-shaped. */
|
|
62
|
+
export function normalizeGlobAsPath(glob) {
|
|
63
|
+
return classifyGlob(glob).prefix;
|
|
64
|
+
}
|
|
65
|
+
function remainderUnder(child, parent) {
|
|
66
|
+
if (child === parent)
|
|
67
|
+
return "";
|
|
68
|
+
const stem = parent.length === 0 ? "" : `${parent}/`;
|
|
69
|
+
if (parent.length === 0)
|
|
70
|
+
return child;
|
|
71
|
+
if (!child.startsWith(stem))
|
|
72
|
+
return null;
|
|
73
|
+
return child.slice(stem.length);
|
|
74
|
+
}
|
|
75
|
+
function depthAllows(depth, remainder) {
|
|
76
|
+
if (depth === "subtree")
|
|
77
|
+
return true;
|
|
78
|
+
if (depth === "exact")
|
|
79
|
+
return remainder.length === 0;
|
|
80
|
+
return remainder.length > 0 && !remainder.includes("/");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* True when two path/glob strings can name a shared tree.
|
|
84
|
+
* `/*` is one segment; `/**` / unstarred file_scope paths are subtrees.
|
|
85
|
+
*/
|
|
86
|
+
export function pathGlobsIntersect(left, right) {
|
|
87
|
+
const a = normalizePath(left).trim();
|
|
88
|
+
const b = normalizePath(right).trim();
|
|
89
|
+
if (a.length === 0 || b.length === 0)
|
|
90
|
+
return false;
|
|
91
|
+
if (a === b)
|
|
92
|
+
return true;
|
|
93
|
+
if (matchPath(a, b) || matchPath(b, a))
|
|
94
|
+
return true;
|
|
95
|
+
const ca = classifyGlob(a);
|
|
96
|
+
const cb = classifyGlob(b);
|
|
97
|
+
if (ca.prefix.length === 0 && cb.prefix.length === 0)
|
|
98
|
+
return true;
|
|
99
|
+
const aUnderB = remainderUnder(ca.prefix, cb.prefix);
|
|
100
|
+
if (aUnderB !== null && depthAllows(cb.depth, aUnderB))
|
|
101
|
+
return true;
|
|
102
|
+
const bUnderA = remainderUnder(cb.prefix, ca.prefix);
|
|
103
|
+
if (bUnderA !== null && depthAllows(ca.depth, bUnderA))
|
|
104
|
+
return true;
|
|
105
|
+
if (ca.leadingDoubleStar && suffixMatch(cb.prefix, ca.prefix))
|
|
106
|
+
return true;
|
|
107
|
+
if (cb.leadingDoubleStar && suffixMatch(ca.prefix, cb.prefix))
|
|
108
|
+
return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function suffixMatch(path, suffix) {
|
|
112
|
+
if (suffix.length === 0 || path.length === 0)
|
|
113
|
+
return false;
|
|
114
|
+
return path === suffix || path.endsWith(`/${suffix}`);
|
|
115
|
+
}
|
|
116
|
+
function surfacePaths(invariant, modulePathGlobs) {
|
|
117
|
+
const paths = [...invariant.contractSurface.paths];
|
|
118
|
+
const unresolvedModuleIds = [];
|
|
119
|
+
for (const moduleId of invariant.contractSurface.moduleIds) {
|
|
120
|
+
const mapped = modulePathGlobs[moduleId];
|
|
121
|
+
if (mapped === undefined || mapped.length === 0) {
|
|
122
|
+
unresolvedModuleIds.push(moduleId);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
paths.push(...mapped);
|
|
126
|
+
}
|
|
127
|
+
return { paths, unresolvedModuleIds };
|
|
128
|
+
}
|
|
129
|
+
function anyPathOverlap(left, right) {
|
|
130
|
+
for (const a of left) {
|
|
131
|
+
for (const b of right) {
|
|
132
|
+
if (pathGlobsIntersect(a, b))
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Invariants whose contract surface intersects `fileScope`.
|
|
140
|
+
* Empty file_scope → empty intersection (no disposition required).
|
|
141
|
+
* Unresolved module ids (no pathGlobs mapping) are treated as applicable.
|
|
142
|
+
*/
|
|
143
|
+
export function applicableProjectInvariants(invariants, fileScope, modulePathGlobs = {}) {
|
|
144
|
+
const scope = fileScope.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
145
|
+
if (scope.length === 0)
|
|
146
|
+
return [];
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const invariant of invariants) {
|
|
149
|
+
const { paths, unresolvedModuleIds } = surfacePaths(invariant, modulePathGlobs);
|
|
150
|
+
if (unresolvedModuleIds.length > 0) {
|
|
151
|
+
out.push({ id: invariant.id, reason: "unresolved-module" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (anyPathOverlap(paths, scope)) {
|
|
155
|
+
const viaModule = invariant.contractSurface.moduleIds.length > 0 && paths.length > 0;
|
|
156
|
+
out.push({
|
|
157
|
+
id: invariant.id,
|
|
158
|
+
reason: viaModule && invariant.contractSurface.paths.length === 0 ? "module" : "path",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
function peelNotApplicable(raw) {
|
|
165
|
+
const notApplicable = [];
|
|
166
|
+
const errors = [];
|
|
167
|
+
if (raw === null || raw === undefined) {
|
|
168
|
+
return { remainder: raw, notApplicable, errors };
|
|
169
|
+
}
|
|
170
|
+
const takeReason = (body) => {
|
|
171
|
+
const nested = asRecord(body.provenance);
|
|
172
|
+
const reason = nested?.reason ?? body.reason;
|
|
173
|
+
return isNonEmptyString(reason) ? reason.trim() : null;
|
|
174
|
+
};
|
|
175
|
+
if (Array.isArray(raw)) {
|
|
176
|
+
const remainder = [];
|
|
177
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
178
|
+
const item = raw[i];
|
|
179
|
+
const loc = `coverage_map[${i}]`;
|
|
180
|
+
const rec = asRecord(item);
|
|
181
|
+
if (rec === null) {
|
|
182
|
+
remainder.push(item);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const disposition = rec.disposition;
|
|
186
|
+
if (typeof disposition === "string" &&
|
|
187
|
+
disposition.trim().toLowerCase() === "not_applicable") {
|
|
188
|
+
const id = rec.parent_requirement_id ?? rec.parentRequirementId ?? rec.id;
|
|
189
|
+
if (!isNonEmptyString(id)) {
|
|
190
|
+
errors.push(`${loc}: parent_requirement_id is required`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const reason = takeReason(rec);
|
|
194
|
+
if (reason === null) {
|
|
195
|
+
errors.push(`${loc}: disposition not_applicable requires reason`);
|
|
196
|
+
}
|
|
197
|
+
notApplicable.push({ id: id.trim(), reason, loc });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
remainder.push(item);
|
|
201
|
+
}
|
|
202
|
+
return { remainder, notApplicable, errors };
|
|
203
|
+
}
|
|
204
|
+
const map = asRecord(raw);
|
|
205
|
+
if (map === null) {
|
|
206
|
+
return { remainder: raw, notApplicable, errors };
|
|
207
|
+
}
|
|
208
|
+
const remainder = {};
|
|
209
|
+
for (const [key, value] of Object.entries(map)) {
|
|
210
|
+
const loc = `coverage_map[${key}]`;
|
|
211
|
+
const rec = asRecord(value);
|
|
212
|
+
if (rec === null) {
|
|
213
|
+
remainder[key] = value;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const disposition = rec.disposition;
|
|
217
|
+
if (typeof disposition === "string" && disposition.trim().toLowerCase() === "not_applicable") {
|
|
218
|
+
const id = rec.parent_requirement_id ?? rec.parentRequirementId ?? key;
|
|
219
|
+
if (!isNonEmptyString(id)) {
|
|
220
|
+
errors.push(`${loc}: parent_requirement_id is required`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const reason = takeReason(rec);
|
|
224
|
+
if (reason === null) {
|
|
225
|
+
errors.push(`${loc}: disposition not_applicable requires reason`);
|
|
226
|
+
}
|
|
227
|
+
notApplicable.push({ id: String(id).trim(), reason, loc });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
remainder[key] = value;
|
|
231
|
+
}
|
|
232
|
+
return { remainder, notApplicable, errors };
|
|
233
|
+
}
|
|
234
|
+
function extractCoverageRaw(draft) {
|
|
235
|
+
const rec = asRecord(draft);
|
|
236
|
+
if (rec === null)
|
|
237
|
+
return undefined;
|
|
238
|
+
if (rec.coverage_map !== undefined || rec.coverageMap !== undefined) {
|
|
239
|
+
return rec.coverage_map ?? rec.coverageMap;
|
|
240
|
+
}
|
|
241
|
+
const plan = asRecord(rec.plan);
|
|
242
|
+
if (plan === null)
|
|
243
|
+
return undefined;
|
|
244
|
+
if (plan.coverage_map !== undefined || plan.coverageMap !== undefined) {
|
|
245
|
+
return plan.coverage_map ?? plan.coverageMap;
|
|
246
|
+
}
|
|
247
|
+
const metadata = asRecord(plan.metadata);
|
|
248
|
+
if (metadata === null)
|
|
249
|
+
return undefined;
|
|
250
|
+
return metadata.coverage_map ?? metadata.coverageMap;
|
|
251
|
+
}
|
|
252
|
+
function extractDeltasRaw(draft) {
|
|
253
|
+
const rec = asRecord(draft);
|
|
254
|
+
if (rec === null)
|
|
255
|
+
return undefined;
|
|
256
|
+
if (rec.behavioral_deltas !== undefined || rec.behavioralDeltas !== undefined) {
|
|
257
|
+
return rec.behavioral_deltas ?? rec.behavioralDeltas;
|
|
258
|
+
}
|
|
259
|
+
const plan = asRecord(rec.plan);
|
|
260
|
+
if (plan === null)
|
|
261
|
+
return undefined;
|
|
262
|
+
return plan.behavioral_deltas ?? plan.behavioralDeltas;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Completeness of coverage_map dispositions for applicable project invariant IDs.
|
|
266
|
+
* Extra coverage_map keys (e.g. parent requirement IDs) are ignored.
|
|
267
|
+
* Empty applicable set is a no-op success.
|
|
268
|
+
*/
|
|
269
|
+
export function validateProjectInvariantCoverage(opts) {
|
|
270
|
+
const applicableIds = [...new Set(opts.applicableIds.map((id) => id.trim()).filter(Boolean))];
|
|
271
|
+
const errors = [];
|
|
272
|
+
const coverageRaw = extractCoverageRaw(opts.draft);
|
|
273
|
+
const peeled = peelNotApplicable(coverageRaw);
|
|
274
|
+
errors.push(...peeled.errors);
|
|
275
|
+
const { entries, errors: mapErrors } = parseCoverageMap(peeled.remainder);
|
|
276
|
+
errors.push(...mapErrors);
|
|
277
|
+
const { byId: deltasById, errors: deltaErrors } = parseBehavioralDeltas(extractDeltasRaw(opts.draft));
|
|
278
|
+
errors.push(...deltaErrors);
|
|
279
|
+
const applicableSet = new Set(applicableIds);
|
|
280
|
+
const byId = new Map();
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
if (!applicableSet.has(entry.parent_requirement_id)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (entry.disposition === "split") {
|
|
286
|
+
errors.push(`coverage_map[${entry.parent_requirement_id}]: disposition split is excluded at project level`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (byId.has(entry.parent_requirement_id)) {
|
|
290
|
+
errors.push(`coverage_map[${entry.parent_requirement_id}]: duplicate coverage entries ` +
|
|
291
|
+
`(one disposition per project invariant ID)`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
byId.set(entry.parent_requirement_id, entry);
|
|
295
|
+
if (entry.disposition === "behavioral_delta") {
|
|
296
|
+
const deltaId = entry.delta_id;
|
|
297
|
+
if (isNonEmptyString(deltaId) && !deltasById.has(deltaId)) {
|
|
298
|
+
errors.push(`coverage_map[${entry.parent_requirement_id}]: behavioral_delta delta_id '${deltaId}' ` +
|
|
299
|
+
`has no linked record in behavioral_deltas`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const addressed = new Set();
|
|
304
|
+
for (const na of peeled.notApplicable) {
|
|
305
|
+
addressed.add(na.id);
|
|
306
|
+
}
|
|
307
|
+
for (const id of byId.keys()) {
|
|
308
|
+
addressed.add(id);
|
|
309
|
+
}
|
|
310
|
+
const missingIds = applicableIds.filter((id) => !addressed.has(id));
|
|
311
|
+
if (missingIds.length > 0) {
|
|
312
|
+
errors.push(`uncovered applicable project invariant IDs: ${missingIds.join(", ")} ` +
|
|
313
|
+
`(declare coverage_map[<id>].disposition as ${PROJECT_INVARIANT_DISPOSITIONS.join("|")})`);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
ok: errors.length === 0,
|
|
317
|
+
applicableIds,
|
|
318
|
+
missingIds,
|
|
319
|
+
errors,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=project-invariant-coverage.js.map
|
package/dist/scope/transition.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public compare entry for extracted intent (#3376 R2 / R5 / #3385 F1–F2).
|
|
3
|
+
*
|
|
4
|
+
* Scalars are byte-equal after canonicalize. Decisions and references[] are
|
|
5
|
+
* guarded-append. Unclassified key only when unknown AND absent from base.
|
|
6
|
+
*/
|
|
7
|
+
import { type IntentPreimage } from "./extract-intent.js";
|
|
8
|
+
export type IntentCompareKind = "intent-drift" | "unclassified-key" | "reference-mutation" | "decisions-mutation" | "repo-not-approved" | "ref-type-not-appendable" | "decision-pointer-missing";
|
|
9
|
+
export interface IntentCompareFinding {
|
|
10
|
+
readonly kind: IntentCompareKind;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly detail: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IntentCompareResult {
|
|
15
|
+
readonly ok: boolean;
|
|
16
|
+
readonly findings: readonly IntentCompareFinding[];
|
|
17
|
+
}
|
|
18
|
+
export interface CompareIntentOptions {
|
|
19
|
+
readonly changedFiles?: readonly string[];
|
|
20
|
+
readonly decisionExists?: (relPath: string) => boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compare a live extraction to the base-committed preimage.
|
|
24
|
+
* One entry point covers scalar equality and guarded append (R2 / R5).
|
|
25
|
+
*/
|
|
26
|
+
export declare function compareExtractedIntent(base: IntentPreimage, live: IntentPreimage, options?: CompareIntentOptions): IntentCompareResult;
|
|
27
|
+
//# sourceMappingURL=compare-intent.d.ts.map
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public compare entry for extracted intent (#3376 R2 / R5 / #3385 F1–F2).
|
|
3
|
+
*
|
|
4
|
+
* Scalars are byte-equal after canonicalize. Decisions and references[] are
|
|
5
|
+
* guarded-append. Unclassified key only when unknown AND absent from base.
|
|
6
|
+
*/
|
|
7
|
+
import { DECISION_FILE_SUFFIX, DECISIONS_DIR_REL } from "../decision/schema.js";
|
|
8
|
+
import { GITHUB_ISSUE_REF_TYPES } from "../intake/reconcile-issues.js";
|
|
9
|
+
import { slugFromGithubIssueUri } from "./extract-intent.js";
|
|
10
|
+
import { sortKeysDeep } from "./intent-digest.js";
|
|
11
|
+
import { classifyPlanPath } from "./known-machine.js";
|
|
12
|
+
const EM_DASH = "\u2014";
|
|
13
|
+
const DECISION_LINE = new RegExp(`^- (${DECISIONS_DIR_REL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/[^\\s]+${DECISION_FILE_SUFFIX.replace(".", "\\.")}) ${EM_DASH} (.+)$`);
|
|
14
|
+
function canon(value) {
|
|
15
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
16
|
+
}
|
|
17
|
+
function asRecord(value) {
|
|
18
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
19
|
+
return null;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function splitDecisionLines(raw) {
|
|
23
|
+
if (typeof raw !== "string")
|
|
24
|
+
return [];
|
|
25
|
+
return raw
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.map((l) => l.trim())
|
|
28
|
+
.filter((l) => l.length > 0);
|
|
29
|
+
}
|
|
30
|
+
function refKey(ref) {
|
|
31
|
+
return `${String(ref.type ?? "")}\n${String(ref.uri ?? "")}`;
|
|
32
|
+
}
|
|
33
|
+
function narrativesOf(plan) {
|
|
34
|
+
return asRecord(plan.narratives) ?? {};
|
|
35
|
+
}
|
|
36
|
+
function compareScalars(basePlan, livePlan, findings) {
|
|
37
|
+
const skip = new Set(["references", "narratives", "items", "parentId"]);
|
|
38
|
+
const keys = new Set([...Object.keys(basePlan), ...Object.keys(livePlan)]);
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
if (skip.has(key))
|
|
41
|
+
continue;
|
|
42
|
+
if (key === "metadata") {
|
|
43
|
+
if (canon(basePlan.metadata) !== canon(livePlan.metadata)) {
|
|
44
|
+
findings.push({
|
|
45
|
+
kind: "intent-drift",
|
|
46
|
+
path: "metadata",
|
|
47
|
+
detail: "extracted metadata drifted from the base preimage",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (canon(basePlan[key]) !== canon(livePlan[key])) {
|
|
53
|
+
findings.push({
|
|
54
|
+
kind: "intent-drift",
|
|
55
|
+
path: key,
|
|
56
|
+
detail: `extracted ${key} drifted from the base preimage`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function compareItems(baseItems, liveItems, findings) {
|
|
62
|
+
if (canon(baseItems ?? []) !== canon(liveItems ?? [])) {
|
|
63
|
+
findings.push({
|
|
64
|
+
kind: "intent-drift",
|
|
65
|
+
path: "items",
|
|
66
|
+
detail: "extracted items drifted from the base preimage",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function compareNarratives(basePlan, livePlan, options, findings) {
|
|
71
|
+
const baseN = narrativesOf(basePlan);
|
|
72
|
+
const liveN = narrativesOf(livePlan);
|
|
73
|
+
const keys = new Set([...Object.keys(baseN), ...Object.keys(liveN)]);
|
|
74
|
+
for (const key of keys) {
|
|
75
|
+
if (key === "Decisions")
|
|
76
|
+
continue;
|
|
77
|
+
if (canon(baseN[key]) !== canon(liveN[key])) {
|
|
78
|
+
findings.push({
|
|
79
|
+
kind: "intent-drift",
|
|
80
|
+
path: `narratives.${key}`,
|
|
81
|
+
detail: `extracted narratives.${key} drifted from the base preimage`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const baseLines = splitDecisionLines(baseN.Decisions);
|
|
86
|
+
const liveLines = splitDecisionLines(liveN.Decisions);
|
|
87
|
+
if (liveLines.length < baseLines.length) {
|
|
88
|
+
findings.push({
|
|
89
|
+
kind: "decisions-mutation",
|
|
90
|
+
path: "narratives.Decisions",
|
|
91
|
+
detail: "Decisions lines were deleted; preimage lines are law",
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0; i < baseLines.length; i += 1) {
|
|
96
|
+
if (liveLines[i] !== baseLines[i]) {
|
|
97
|
+
findings.push({
|
|
98
|
+
kind: "decisions-mutation",
|
|
99
|
+
path: "narratives.Decisions",
|
|
100
|
+
detail: "existing Decisions lines were edited; only append is allowed",
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const changed = new Set((options.changedFiles ?? []).map((p) => p.replace(/\\/g, "/")));
|
|
106
|
+
for (const line of liveLines.slice(baseLines.length)) {
|
|
107
|
+
const m = DECISION_LINE.exec(line);
|
|
108
|
+
if (m === null) {
|
|
109
|
+
findings.push({
|
|
110
|
+
kind: "decisions-mutation",
|
|
111
|
+
path: "narratives.Decisions",
|
|
112
|
+
detail: `appended Decisions line is not '- <${DECISIONS_DIR_REL}>/…${DECISION_FILE_SUFFIX} ${EM_DASH} <summary>'`,
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const rel = m[1] ?? "";
|
|
117
|
+
const onBaseOrChange = changed.has(rel) || Boolean(options.decisionExists?.(rel));
|
|
118
|
+
if (!onBaseOrChange) {
|
|
119
|
+
findings.push({
|
|
120
|
+
kind: "decision-pointer-missing",
|
|
121
|
+
path: "narratives.Decisions",
|
|
122
|
+
detail: `Decisions pointer ${rel} is not on base and not in the change set`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function compareReferences(base, live, findings) {
|
|
128
|
+
const baseRefs = Array.isArray(base.plan.references)
|
|
129
|
+
? base.plan.references
|
|
130
|
+
: [];
|
|
131
|
+
const liveRefs = Array.isArray(live.plan.references)
|
|
132
|
+
? live.plan.references
|
|
133
|
+
: [];
|
|
134
|
+
const liveByKey = new Map();
|
|
135
|
+
for (const ref of liveRefs) {
|
|
136
|
+
liveByKey.set(refKey(ref), ref);
|
|
137
|
+
}
|
|
138
|
+
for (const ref of baseRefs) {
|
|
139
|
+
const key = refKey(ref);
|
|
140
|
+
const liveRef = liveByKey.get(key);
|
|
141
|
+
if (liveRef === undefined) {
|
|
142
|
+
findings.push({
|
|
143
|
+
kind: "reference-mutation",
|
|
144
|
+
path: "references",
|
|
145
|
+
detail: `origin reference removed: ${String(ref.uri ?? "")}`,
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (canon(ref) !== canon(liveRef)) {
|
|
150
|
+
findings.push({
|
|
151
|
+
kind: "reference-mutation",
|
|
152
|
+
path: "references",
|
|
153
|
+
detail: `origin reference edited: ${String(ref.uri ?? "")}`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const baseKeys = new Set(baseRefs.map(refKey));
|
|
158
|
+
const approved = new Set(base.approvedRepos.map((s) => s.toLowerCase()));
|
|
159
|
+
for (const ref of liveRefs) {
|
|
160
|
+
if (baseKeys.has(refKey(ref)))
|
|
161
|
+
continue;
|
|
162
|
+
const type = String(ref.type ?? "");
|
|
163
|
+
if (!GITHUB_ISSUE_REF_TYPES.has(type)) {
|
|
164
|
+
findings.push({
|
|
165
|
+
kind: "ref-type-not-appendable",
|
|
166
|
+
path: "references",
|
|
167
|
+
detail: `appended reference type ${JSON.stringify(type)} is not in GITHUB_ISSUE_REF_TYPES`,
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const uri = typeof ref.uri === "string" ? ref.uri : "";
|
|
172
|
+
const slug = slugFromGithubIssueUri(uri);
|
|
173
|
+
if (slug === null || !approved.has(slug)) {
|
|
174
|
+
findings.push({
|
|
175
|
+
kind: "repo-not-approved",
|
|
176
|
+
path: "references",
|
|
177
|
+
detail: `appended github-issue URL is not in the base preimage approvedRepos: ${uri}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function compareUnknownKeys(base, live, findings) {
|
|
183
|
+
const baseUnknown = new Set(base.unknownPaths);
|
|
184
|
+
const basePlan = asRecord(base.plan) ?? {};
|
|
185
|
+
for (const path of live.unknownPaths) {
|
|
186
|
+
if (baseUnknown.has(path))
|
|
187
|
+
continue;
|
|
188
|
+
const top = path.split(".")[0] ?? path;
|
|
189
|
+
if (top in basePlan && classifyPlanPath(top) === "unknown")
|
|
190
|
+
continue;
|
|
191
|
+
const parts = path.split(".");
|
|
192
|
+
let cursor = base.plan;
|
|
193
|
+
let present = true;
|
|
194
|
+
for (const part of parts) {
|
|
195
|
+
const rec = asRecord(cursor);
|
|
196
|
+
if (rec === null || !(part in rec)) {
|
|
197
|
+
present = false;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
cursor = rec[part];
|
|
201
|
+
}
|
|
202
|
+
if (present)
|
|
203
|
+
continue;
|
|
204
|
+
findings.push({
|
|
205
|
+
kind: "unclassified-key",
|
|
206
|
+
path,
|
|
207
|
+
detail: `unclassified key ${path} is absent from the base preimage`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Compare a live extraction to the base-committed preimage.
|
|
213
|
+
* One entry point covers scalar equality and guarded append (R2 / R5).
|
|
214
|
+
*/
|
|
215
|
+
export function compareExtractedIntent(base, live, options = {}) {
|
|
216
|
+
const findings = [];
|
|
217
|
+
const basePlan = asRecord(base.plan) ?? {};
|
|
218
|
+
const livePlan = asRecord(live.plan) ?? {};
|
|
219
|
+
if (canon(basePlan.parentId) !== canon(livePlan.parentId)) {
|
|
220
|
+
findings.push({
|
|
221
|
+
kind: "intent-drift",
|
|
222
|
+
path: "parentId",
|
|
223
|
+
detail: "parent identity changed (silent reparent)",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
compareScalars(basePlan, livePlan, findings);
|
|
227
|
+
compareItems(basePlan.items, livePlan.items, findings);
|
|
228
|
+
compareNarratives(basePlan, livePlan, options, findings);
|
|
229
|
+
compareReferences(base, live, findings);
|
|
230
|
+
compareUnknownKeys(base, live, findings);
|
|
231
|
+
return { ok: findings.length === 0, findings };
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=compare-intent.js.map
|
|
@@ -27,8 +27,15 @@ export interface ApprovedScopeRecord {
|
|
|
27
27
|
readonly mintedAt: string;
|
|
28
28
|
readonly mintedVia?: string;
|
|
29
29
|
};
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Legacy optional body digest. Wave 1 (#3384 F4) does not write this on new
|
|
32
|
+
* records. Do not treat it as authority (Wave 2 #3385 / R6).
|
|
33
|
+
*/
|
|
31
34
|
readonly xbriefBodyDigest?: string;
|
|
35
|
+
/** Checksum of the base-committed `.intent.json` preimage (#3385). */
|
|
36
|
+
readonly intentDigest?: string;
|
|
37
|
+
/** Digest algorithm name; Wave 2 writes `intent-extract-v1`. */
|
|
38
|
+
readonly digestAlgo?: string;
|
|
32
39
|
}
|
|
33
40
|
export declare const APPROVED_SCOPE_DIR = ".deft/approved-scope";
|
|
34
41
|
/** Normalize and sort scope paths for stable digests. */
|
|
@@ -42,14 +49,24 @@ export declare function extractFileScope(payload: unknown): string[];
|
|
|
42
49
|
/** Extract plan.id from an xBRIEF payload. */
|
|
43
50
|
export declare function extractPlanId(payload: unknown): string | null;
|
|
44
51
|
export declare function approvedScopeDir(projectRoot: string): string;
|
|
52
|
+
export declare function approvedScopeSafePlanId(planId: string): string;
|
|
45
53
|
export declare function approvedScopeRecordPath(projectRoot: string, planId: string): string;
|
|
54
|
+
/** Repo-relative preimage path. */
|
|
55
|
+
export declare function approvedScopeIntentRel(planId: string): string;
|
|
56
|
+
export declare function approvedScopeIntentPath(projectRoot: string, planId: string): string;
|
|
46
57
|
/** Build an ApprovedScopeRecord from xBRIEF payload + metadata. */
|
|
47
58
|
export declare function buildApprovedScopeRecord(input: {
|
|
48
59
|
readonly xbriefRelPath: string;
|
|
49
60
|
readonly payload: unknown;
|
|
50
61
|
readonly approvedAt?: string;
|
|
51
62
|
readonly humanApproval?: ApprovedScopeRecord["humanApproval"];
|
|
63
|
+
/**
|
|
64
|
+
* Ignored. Wave 1 (#3384 F4) no longer writes `xbriefBodyDigest` on new records.
|
|
65
|
+
* Kept so existing callers do not have to drop the field in the same change.
|
|
66
|
+
*/
|
|
52
67
|
readonly xbriefRawText?: string;
|
|
68
|
+
readonly intentDigest?: string;
|
|
69
|
+
readonly digestAlgo?: string;
|
|
53
70
|
}): ApprovedScopeRecord;
|
|
54
71
|
/** Persist approved-scope record (activation / renewed human approval). */
|
|
55
72
|
export declare function writeApprovedScopeRecord(projectRoot: string, record: ApprovedScopeRecord): string;
|