@cassiomc1/forgeloop 1.6.3 → 1.6.5

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.
Files changed (127) hide show
  1. package/DOCS_INDEX.md +26 -4
  2. package/EXECUTION_STATE.md +7 -0
  3. package/LOOP_ENGINEERING.md +58 -0
  4. package/LOOP_SYSTEM_DESIGN.md +54 -0
  5. package/ORCHESTRATOR_INTEGRATION.md +23 -0
  6. package/PROTOCOL_INTEGRATION.md +36 -0
  7. package/QUALITY_SCORECARD.md +21 -0
  8. package/README.md +55 -0
  9. package/TERMINOLOGY.md +11 -0
  10. package/THREAT_MODEL.md +7 -0
  11. package/completions/_forgeloop +85 -0
  12. package/completions/forgeloop.bash +173 -0
  13. package/completions/forgeloop.fish +544 -0
  14. package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
  15. package/docs/ARTIFACT_REFERENCE.md +226 -0
  16. package/docs/CLI_REFERENCE.md +308 -2
  17. package/docs/CODE_ATTESTATION.md +141 -0
  18. package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
  19. package/docs/DOCUMENTATION_GUIDE.md +13 -11
  20. package/docs/GETTING_STARTED.md +98 -1
  21. package/docs/MCP.md +8 -0
  22. package/docs/PLATFORM_ADAPTERS.md +69 -0
  23. package/docs/RECIPES.md +138 -0
  24. package/docs/RELEASE_CHECKLIST.md +54 -0
  25. package/docs/REVISION_PROVIDERS.md +136 -0
  26. package/docs/SIGNING_PROVIDERS.md +83 -0
  27. package/docs/TROUBLESHOOTING.md +184 -0
  28. package/docs/UNIVERSAL_INTEGRATION.md +11 -0
  29. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
  30. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
  31. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
  32. package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
  33. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  34. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
  35. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
  36. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
  37. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
  38. package/docs/diagrams/README.md +30 -22
  39. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
  40. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
  41. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
  42. package/docs/diagrams/manifest.json +42 -0
  43. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
  44. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
  45. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
  46. package/integrations/generic-ci/verify.sh +14 -0
  47. package/package.json +32 -9
  48. package/schemas/attestation-verification-result.schema.json +34 -0
  49. package/schemas/code-attestation.schema.json +61 -0
  50. package/schemas/code-manifest.schema.json +57 -0
  51. package/schemas/config.schema.json +57 -1
  52. package/schemas/handoff-envelope.schema.json +57 -0
  53. package/schemas/in-toto-statement.schema.json +32 -0
  54. package/schemas/responsibility.schema.json +37 -0
  55. package/schemas/verification-scope.schema.json +26 -0
  56. package/schemas/workspace-binding.schema.json +20 -0
  57. package/scripts/CI_VALIDATORS.md +3 -0
  58. package/scripts/benchmark-cli-startup.mjs +60 -0
  59. package/scripts/check-changelog-freshness.mjs +88 -0
  60. package/scripts/check-critical-coverage.mjs +58 -0
  61. package/scripts/generate-agent-protocol-summary.mjs +141 -0
  62. package/scripts/generate-shell-completions.mjs +132 -0
  63. package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
  64. package/src/cli.js +114 -6
  65. package/src/commands/attestation-create.js +30 -0
  66. package/src/commands/attestation-status.js +12 -0
  67. package/src/commands/attestation-verify-range.js +32 -0
  68. package/src/commands/attestation-verify.js +23 -0
  69. package/src/commands/handoff-create.js +9 -0
  70. package/src/commands/handoff-list.js +12 -0
  71. package/src/commands/handoff-show.js +10 -0
  72. package/src/commands/responsibility-set.js +20 -0
  73. package/src/commands/responsibility-status.js +12 -0
  74. package/src/commands/run-check.js +43 -3
  75. package/src/commands/task-repair-legacy-recovery.js +1 -2
  76. package/src/commands/verify-scope.js +9 -0
  77. package/src/commands/workspace-bind.js +17 -0
  78. package/src/commands/workspace-status.js +19 -0
  79. package/src/core/artifact-registry.js +84 -0
  80. package/src/core/attestation-coverage.js +134 -0
  81. package/src/core/attestation-verifier.js +227 -0
  82. package/src/core/attestation.js +263 -0
  83. package/src/core/audit.js +67 -1
  84. package/src/core/bundles.js +210 -9
  85. package/src/core/cli-command-definitions.js +196 -0
  86. package/src/core/code-manifest.js +293 -0
  87. package/src/core/command-executors.js +79 -0
  88. package/src/core/command-input.js +69 -18
  89. package/src/core/command-runtime.js +2 -1
  90. package/src/core/completion.js +170 -8
  91. package/src/core/config.js +60 -2
  92. package/src/core/continuity-cli-options.js +9 -3
  93. package/src/core/continuity-reconciliation.js +20 -0
  94. package/src/core/diagnostic-projection.js +3 -1
  95. package/src/core/error-codes.js +146 -0
  96. package/src/core/events.js +109 -13
  97. package/src/core/exit-codes.js +22 -0
  98. package/src/core/handoff.js +208 -0
  99. package/src/core/information-gain-projection.js +3 -3
  100. package/src/core/integration-invocation-policy.js +40 -0
  101. package/src/core/integration-resources.js +54 -0
  102. package/src/core/next-action-phases.js +968 -0
  103. package/src/core/next-action.js +32 -951
  104. package/src/core/phase.js +29 -1
  105. package/src/core/protocol-info.js +41 -1
  106. package/src/core/repository.js +256 -11
  107. package/src/core/responsibility.js +265 -0
  108. package/src/core/resumability.js +27 -1
  109. package/src/core/revision/git.js +207 -0
  110. package/src/core/revision/provider.js +90 -0
  111. package/src/core/revision/registry.js +5 -0
  112. package/src/core/route-artifact.js +1 -1
  113. package/src/core/schema-validation.js +8 -0
  114. package/src/core/signing/none.js +23 -0
  115. package/src/core/signing/provider.js +21 -0
  116. package/src/core/signing/registry.js +7 -0
  117. package/src/core/signing/sigstore.js +158 -0
  118. package/src/core/task-command.js +9 -1
  119. package/src/core/task-paths.js +72 -0
  120. package/src/core/templates.js +8 -0
  121. package/src/core/verification-scope-capability.js +179 -0
  122. package/src/core/verification-scope.js +271 -0
  123. package/src/core/workspace-binding.js +211 -0
  124. package/src/integration.d.ts +91 -0
  125. package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
  126. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
  127. package/docs/RELEASE_CHECKLIST_1_6_1.md +0 -121
@@ -20,6 +20,11 @@ export const TASK_ARTIFACT_FILES = Object.freeze({
20
20
  evaluations: "evaluations",
21
21
  policySnapshot: "policy-snapshot.json",
22
22
  recovery: "recovery.json",
23
+ workspaceBinding: "workspace-binding.json",
24
+ handoffs: "handoffs",
25
+ responsibility: "responsibility.json",
26
+ verificationScope: "verification-scope.json",
27
+ attestations: "attestations",
23
28
  });
24
29
 
25
30
  export const POLICY_ROOT = ".forgeloop/policy";
@@ -99,6 +104,65 @@ export function taskEvaluationPath(taskId, evaluationId) {
99
104
  return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.evaluations}/${evaluationId}.json`;
100
105
  }
101
106
 
107
+ function assertArtifactId(value, pattern, label) {
108
+ if (typeof value !== "string" || !pattern.test(value)) {
109
+ throw new Error(`Invalid ${label}: ${value}`);
110
+ }
111
+ return value;
112
+ }
113
+
114
+ export function taskWorkspaceBindingPath(taskId) {
115
+ return taskArtifactPath(taskId, "workspaceBinding");
116
+ }
117
+
118
+ export function taskResponsibilityPath(taskId) {
119
+ return taskArtifactPath(taskId, "responsibility");
120
+ }
121
+
122
+ export function taskVerificationScopePath(taskId) {
123
+ return taskArtifactPath(taskId, "verificationScope");
124
+ }
125
+
126
+ export function taskHandoffDirectory(taskId) {
127
+ return taskArtifactPath(taskId, "handoffs");
128
+ }
129
+
130
+ export function taskHandoffPath(taskId, handoffId) {
131
+ assertArtifactId(handoffId, /^handoff-[A-Za-z0-9_-]+$/, "handoff ID");
132
+ return `${taskHandoffDirectory(taskId)}/${handoffId}.json`;
133
+ }
134
+
135
+ export function taskAttestationDirectory(taskId) {
136
+ return taskArtifactPath(taskId, "attestations");
137
+ }
138
+
139
+ export function taskCodeManifestPath(taskId) {
140
+ return `${taskAttestationDirectory(taskId)}/code-manifest.json`;
141
+ }
142
+
143
+ export function taskAttestationStatementPath(taskId) {
144
+ return `${taskAttestationDirectory(taskId)}/statement.json`;
145
+ }
146
+
147
+ export function taskAttestationBundlePath(taskId) {
148
+ return `${taskAttestationDirectory(taskId)}/statement.sigstore.json`;
149
+ }
150
+
151
+ export function taskAttestationHistoryDirectory(taskId, verificationCycle) {
152
+ if (!Number.isInteger(verificationCycle) || verificationCycle < 1) {
153
+ throw new Error(`Invalid attestation verification cycle: ${verificationCycle}`);
154
+ }
155
+ return `${taskAttestationDirectory(taskId)}/history/cycle-${verificationCycle}`;
156
+ }
157
+
158
+ export function taskCodeManifestHistoryPath(taskId, verificationCycle) {
159
+ return `${taskAttestationHistoryDirectory(taskId, verificationCycle)}/code-manifest.json`;
160
+ }
161
+
162
+ export function taskAttestationStatementHistoryPath(taskId, verificationCycle) {
163
+ return `${taskAttestationHistoryDirectory(taskId, verificationCycle)}/statement.json`;
164
+ }
165
+
102
166
  export function taskLockPath(taskId) {
103
167
  assertTaskId(taskId);
104
168
  return `${TASK_LOCK_ROOT}/${taskStorageKey(taskId)}.lock`;
@@ -132,5 +196,13 @@ export function buildTaskArtifactPaths(taskId) {
132
196
  lock: taskLockPath(taskId),
133
197
  policySnapshot: `${dir}/${TASK_ARTIFACT_FILES.policySnapshot}`,
134
198
  recovery: `${dir}/${TASK_ARTIFACT_FILES.recovery}`,
199
+ workspaceBinding: `${dir}/${TASK_ARTIFACT_FILES.workspaceBinding}`,
200
+ handoffs: `${dir}/${TASK_ARTIFACT_FILES.handoffs}`,
201
+ responsibility: `${dir}/${TASK_ARTIFACT_FILES.responsibility}`,
202
+ verificationScope: `${dir}/${TASK_ARTIFACT_FILES.verificationScope}`,
203
+ attestations: `${dir}/${TASK_ARTIFACT_FILES.attestations}`,
204
+ codeManifest: taskCodeManifestPath(taskId),
205
+ attestationStatement: taskAttestationStatementPath(taskId),
206
+ attestationBundle: taskAttestationBundlePath(taskId),
135
207
  });
136
208
  }
@@ -75,6 +75,14 @@ export const TEMPLATE_PATHS = [
75
75
  "schemas/capability-policy.schema.json",
76
76
  "schemas/trajectory-evaluation.schema.json",
77
77
  "schemas/trajectory-scenario.schema.json",
78
+ "schemas/workspace-binding.schema.json",
79
+ "schemas/handoff-envelope.schema.json",
80
+ "schemas/responsibility.schema.json",
81
+ "schemas/verification-scope.schema.json",
82
+ "schemas/code-manifest.schema.json",
83
+ "schemas/code-attestation.schema.json",
84
+ "schemas/attestation-verification-result.schema.json",
85
+ "schemas/in-toto-statement.schema.json",
78
86
  ];
79
87
 
80
88
  export function getPackageRoot() {
@@ -0,0 +1,179 @@
1
+ import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact } from "./artifacts.js";
2
+
3
+ export const SCOPED_CHECKER_SCOPE_MODE = "PATH_ARGUMENTS";
4
+ export const SCOPED_CHECKER_PATH_INSERTION = "APPEND";
5
+ export const E_VERIFICATION_SCOPE_UNRESOLVED = "E_VERIFICATION_SCOPE_UNRESOLVED";
6
+
7
+ function capabilityError(message, cause = null) {
8
+ const error = new Error(message);
9
+ error.name = "VerificationScopeCapabilityError";
10
+ error.code = E_VERIFICATION_SCOPE_UNRESOLVED;
11
+ if (cause) error.cause = cause;
12
+ return error;
13
+ }
14
+
15
+ function nonEmptyString(value, label) {
16
+ if (typeof value !== "string" || value.trim() === "") {
17
+ throw capabilityError(`${label} must be a non-empty string`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function argvPrefix(value, label) {
23
+ if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.trim() === "")) {
24
+ throw capabilityError(`${label} must be a non-empty exact argv prefix`);
25
+ }
26
+ return [...value];
27
+ }
28
+
29
+ function assertDescriptorKeys(value, label) {
30
+ const allowed = new Set(["checkId", "scopeMode", "argvPrefix", "pathInsertion"]);
31
+ const unexpected = Object.keys(value).find((key) => !allowed.has(key));
32
+ if (unexpected) throw capabilityError(`${label} contains unknown property: ${unexpected}`);
33
+ }
34
+
35
+ export function normalizeScopedCheckerDescriptor(value, label = "verification.checkers[]") {
36
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37
+ throw capabilityError(`${label} must be an object`);
38
+ }
39
+ assertDescriptorKeys(value, label);
40
+ const checkId = nonEmptyString(value.checkId, `${label}.checkId`).trim();
41
+ if (value.scopeMode !== SCOPED_CHECKER_SCOPE_MODE) {
42
+ throw capabilityError(`${label}.scopeMode must be ${SCOPED_CHECKER_SCOPE_MODE}`);
43
+ }
44
+ if (value.pathInsertion !== SCOPED_CHECKER_PATH_INSERTION) {
45
+ throw capabilityError(`${label}.pathInsertion must be ${SCOPED_CHECKER_PATH_INSERTION}`);
46
+ }
47
+ return {
48
+ checkId,
49
+ scopeMode: SCOPED_CHECKER_SCOPE_MODE,
50
+ argvPrefix: argvPrefix(value.argvPrefix, `${label}.argvPrefix`),
51
+ pathInsertion: SCOPED_CHECKER_PATH_INSERTION,
52
+ };
53
+ }
54
+
55
+ export function normalizeScopedCheckerDescriptors(value, label = "verification.checkers") {
56
+ if (!Array.isArray(value)) throw capabilityError(`${label} must be an array`);
57
+ const descriptors = value.map((item, index) => normalizeScopedCheckerDescriptor(item, `${label}[${index}]`));
58
+ const seen = new Set();
59
+ for (const descriptor of descriptors) {
60
+ if (seen.has(descriptor.checkId)) throw capabilityError(`${label} contains duplicate checkId: ${descriptor.checkId}`);
61
+ seen.add(descriptor.checkId);
62
+ }
63
+ return descriptors.sort((left, right) => left.checkId.localeCompare(right.checkId));
64
+ }
65
+
66
+ export function normalizeVerificationConfiguration(value, label = "verification") {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
68
+ throw capabilityError(`${label} must be an object`);
69
+ }
70
+ const unexpected = Object.keys(value).find((key) => key !== "checkers");
71
+ if (unexpected) throw capabilityError(`${label} contains unknown property: ${unexpected}`);
72
+ return {
73
+ checkers: normalizeScopedCheckerDescriptors(value.checkers ?? [], `${label}.checkers`),
74
+ };
75
+ }
76
+
77
+ export function scopedCheckerCapabilitiesFingerprint(checkers = []) {
78
+ return canonicalFingerprint({
79
+ schemaVersion: 1,
80
+ scopeMode: SCOPED_CHECKER_SCOPE_MODE,
81
+ checkers: normalizeScopedCheckerDescriptors(checkers),
82
+ });
83
+ }
84
+
85
+ export async function readScopedCheckerCapabilities(target, packageRoot) {
86
+ try {
87
+ const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot);
88
+ const verification = artifact.value.verification;
89
+ const checkers = verification
90
+ ? normalizeVerificationConfiguration(verification).checkers
91
+ : [];
92
+ return {
93
+ valid: true,
94
+ source: "project-config",
95
+ checkers,
96
+ fingerprint: scopedCheckerCapabilitiesFingerprint(checkers),
97
+ error: null,
98
+ };
99
+ } catch (error) {
100
+ if (error.code === "ARTIFACT_MISSING") {
101
+ return {
102
+ valid: true,
103
+ source: "project-config",
104
+ checkers: [],
105
+ fingerprint: scopedCheckerCapabilitiesFingerprint([]),
106
+ error: null,
107
+ };
108
+ }
109
+ return {
110
+ valid: false,
111
+ source: "project-config",
112
+ checkers: [],
113
+ fingerprint: null,
114
+ error,
115
+ };
116
+ }
117
+ }
118
+
119
+ function equalArgv(left, right) {
120
+ return Array.isArray(left)
121
+ && Array.isArray(right)
122
+ && left.length === right.length
123
+ && left.every((item, index) => item === right[index]);
124
+ }
125
+
126
+ export function scopedCheckerArgv(checker, selectedPaths) {
127
+ const descriptor = normalizeScopedCheckerDescriptor(checker, "scoped checker");
128
+ if (!Array.isArray(selectedPaths) || selectedPaths.some((item) => typeof item !== "string" || item.length === 0)) {
129
+ throw capabilityError("Verification scope selectedPaths must be an array of non-empty strings");
130
+ }
131
+ return [...descriptor.argvPrefix, ...selectedPaths];
132
+ }
133
+
134
+ export async function bindVerificationScopeCommand({
135
+ target,
136
+ packageRoot,
137
+ checkId,
138
+ argv,
139
+ scope,
140
+ capabilities = null,
141
+ } = {}) {
142
+ if (!scope || scope.resolvedMode === "FULL") {
143
+ return {
144
+ argv: [...argv],
145
+ checker: null,
146
+ capabilityFingerprint: null,
147
+ };
148
+ }
149
+ if (!["CHANGED", "CLAIMED"].includes(scope.resolvedMode)) {
150
+ throw capabilityError("Verification scope has no resolved narrow mode");
151
+ }
152
+ const resolvedCapabilities = capabilities ?? await readScopedCheckerCapabilities(target, packageRoot);
153
+ if (!resolvedCapabilities.valid) {
154
+ throw capabilityError("Trusted scoped-checker capability is invalid", resolvedCapabilities.error);
155
+ }
156
+ const checker = resolvedCapabilities.checkers.find((item) => item.checkId === checkId);
157
+ if (!checker) {
158
+ throw capabilityError(`No trusted scoped checker is registered for check ${checkId}`);
159
+ }
160
+ const expectedArgv = scopedCheckerArgv(checker, scope.selectedPaths);
161
+ if (!equalArgv(argv, expectedArgv)) {
162
+ const error = capabilityError("Scoped checker argv does not match the canonical scope binding");
163
+ error.reason = "ARGV_MISMATCH";
164
+ throw error;
165
+ }
166
+ return {
167
+ argv: expectedArgv,
168
+ checker,
169
+ capabilityFingerprint: resolvedCapabilities.fingerprint,
170
+ };
171
+ }
172
+
173
+ export async function requireTrustedScopedChecker({ target, packageRoot } = {}) {
174
+ const capabilities = await readScopedCheckerCapabilities(target, packageRoot);
175
+ if (!capabilities.valid || capabilities.checkers.length === 0) {
176
+ throw capabilityError("A trusted scoped-checker capability is required for a narrow verification scope", capabilities.error);
177
+ }
178
+ return capabilities;
179
+ }
@@ -0,0 +1,271 @@
1
+ import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
+ import { assertSchema, readSchema } from "./schema-validation.js";
3
+ import { getPackageRoot } from "./templates.js";
4
+ import { currentRepositoryFingerprint } from "./repository.js";
5
+ import { resolveRevisionProvider } from "./revision/provider.js";
6
+ import { REVISION_PROVIDERS } from "./revision/registry.js";
7
+ import { readScopedCheckerCapabilities } from "./verification-scope-capability.js";
8
+ import { readContract } from "./contract.js";
9
+ import { readPersistedRoute } from "./route-artifact.js";
10
+ import { readWorkState } from "./work-state.js";
11
+ import { resolveTaskClaimState } from "./task-claim-state.js";
12
+ import { taskVerificationScopePath } from "./task-paths.js";
13
+ import { ensureWithin, fileExists } from "./filesystem.js";
14
+ import { appendProtocolEvent } from "./events.js";
15
+ import { withTaskTransaction } from "./transaction.js";
16
+ import { assertTaskMutationAllowed } from "./task-claim-state.js";
17
+ import { assertWorkspaceBinding } from "./workspace-binding.js";
18
+ import path from "node:path";
19
+
20
+ const MODES = Object.freeze(["AUTO", "CHANGED", "CLAIMED", "FULL"]);
21
+
22
+ function scopeError(code, message, artifacts = []) {
23
+ const error = new Error(message);
24
+ error.name = "VerificationScopeError";
25
+ error.code = code;
26
+ error.artifacts = artifacts;
27
+ return error;
28
+ }
29
+
30
+ function normalizeScopePath(value, label = "path") {
31
+ if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
32
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must be a non-empty relative path`);
33
+ }
34
+ const replaced = value.replaceAll("\\", "/");
35
+ if (replaced.startsWith("/") || /^[A-Za-z]:\//u.test(replaced)) {
36
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must be relative: ${value}`);
37
+ }
38
+ const normalized = path.posix.normalize(replaced);
39
+ const canonical = normalized.replace(/^\.\//u, "");
40
+ if (canonical === ".." || canonical.startsWith("../") || canonical === ".forgeloop" || canonical.startsWith(".forgeloop/")) {
41
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} is outside the source boundary: ${value}`);
42
+ }
43
+ if (replaced !== canonical && replaced !== `./${canonical}`) {
44
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must use a canonical relative path: ${value}`);
45
+ }
46
+ return canonical;
47
+ }
48
+
49
+ function sortedPaths(paths, label = "paths") {
50
+ if (!Array.isArray(paths)) throw scopeError("E_VERIFICATION_SCOPE_INVALID", `${label} must be an array`);
51
+ return [...new Set(paths.map((value, index) => normalizeScopePath(value, `${label}[${index}]`)))].sort();
52
+ }
53
+
54
+ function pathCovered(pathValue, claims) {
55
+ return claims.includes(".") || claims.some((claim) => pathValue === claim || pathValue.startsWith(`${claim}/`));
56
+ }
57
+
58
+ function requirementText(requirement) {
59
+ return typeof requirement === "string" ? requirement : requirement?.text ?? "";
60
+ }
61
+
62
+ function requiresFullVerification(contract) {
63
+ return [...(contract?.value?.verification ?? []), ...(contract?.value?.successCriteria ?? [])]
64
+ .some((requirement) => /(?:full|entire|whole|global)\s+(?:project|repository|repo|suite|verification)|all\s+(?:tests|checks)/iu.test(requirementText(requirement)));
65
+ }
66
+
67
+ async function currentChangedPathsFromRevisionProvider(target) {
68
+ try {
69
+ const provider = await resolveRevisionProvider({ target, registry: REVISION_PROVIDERS });
70
+ const entries = await provider.getChangedEntries({ target, headRevision: "WORKTREE" });
71
+ return [...new Set(entries
72
+ .flatMap((entry) => [entry.path, entry.sourcePath].filter(Boolean)))].sort((left, right) => left.localeCompare(right));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async function currentInputs(target, { taskId, packageRoot }) {
79
+ const [contract, route, state, claims, repositoryFingerprint, changedPaths, checkerCapabilities] = await Promise.all([
80
+ readContract(target, packageRoot, { taskId }),
81
+ readPersistedRoute(target, packageRoot, { taskId }),
82
+ readWorkState(target, { packageRoot, taskId }),
83
+ resolveTaskClaimState(target, { packageRoot, taskId }),
84
+ currentRepositoryFingerprint(target),
85
+ currentChangedPathsFromRevisionProvider(target),
86
+ readScopedCheckerCapabilities(target, packageRoot),
87
+ ]);
88
+ return {
89
+ contract,
90
+ route,
91
+ state,
92
+ claims,
93
+ claimsFingerprint: canonicalFingerprint(claims.effectiveWriteClaims ?? []),
94
+ repositoryFingerprint,
95
+ changedPaths: changedPaths === null ? null : sortedPaths(changedPaths),
96
+ checkerCapabilities,
97
+ };
98
+ }
99
+
100
+ function normalizeMode(value) {
101
+ const mode = String(value ?? "AUTO").toUpperCase();
102
+ if (!MODES.includes(mode)) throw scopeError("E_VERIFICATION_SCOPE_INVALID", `Unsupported verification scope mode: ${value}`);
103
+ return mode;
104
+ }
105
+
106
+ export async function validateVerificationScope(scope, packageRoot = getPackageRoot()) {
107
+ try {
108
+ assertSchema(scope, await readSchema("verification-scope", packageRoot), "verification scope");
109
+ } catch (error) {
110
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", error.message);
111
+ }
112
+ if (scope.taskId.length === 0 || scope.requestedMode === "IMPACTED" || scope.resolvedMode === "IMPACTED") {
113
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", "IMPACTED is reserved for an explicit future mapping capability");
114
+ }
115
+ const canonical = {
116
+ ...scope,
117
+ changedPaths: sortedPaths(scope.changedPaths),
118
+ claimedPaths: sortedPaths(scope.claimedPaths),
119
+ selectedPaths: sortedPaths(scope.selectedPaths),
120
+ reasons: [...scope.reasons],
121
+ };
122
+ return canonical;
123
+ }
124
+
125
+ export async function readVerificationScope(target, { taskId, packageRoot = getPackageRoot(), scopePath = null } = {}) {
126
+ const relativePath = scopePath ?? taskVerificationScopePath(taskId);
127
+ if (!(await fileExists(ensureWithin(target, relativePath)))) {
128
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `Verification scope is missing: ${relativePath}`, [relativePath]);
129
+ }
130
+ try {
131
+ const artifact = await readJsonArtifact(target, relativePath, "verification-scope", packageRoot);
132
+ return { ...artifact, value: await validateVerificationScope(artifact.value, packageRoot) };
133
+ } catch (error) {
134
+ if (error.code === "E_VERIFICATION_SCOPE_INVALID") throw error;
135
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", error.message, [relativePath]);
136
+ }
137
+ }
138
+
139
+ export async function validateVerificationScopeFreshness(target, {
140
+ taskId,
141
+ scope,
142
+ packageRoot = getPackageRoot(),
143
+ } = {}) {
144
+ const current = await currentInputs(target, { taskId, packageRoot });
145
+ const mismatches = [];
146
+ if (scope.taskId !== taskId) mismatches.push("taskId");
147
+ if (scope.verificationCycle !== (current.state?.verificationCycle ?? 1)) mismatches.push("verificationCycle");
148
+ if (scope.contractFingerprint !== current.contract.fingerprint) mismatches.push("contractFingerprint");
149
+ if (JSON.stringify(scope.repositoryFingerprint) !== JSON.stringify(current.repositoryFingerprint)) mismatches.push("repositoryFingerprint");
150
+ if (JSON.stringify(sortedPaths(scope.changedPaths)) !== JSON.stringify(current.changedPaths)) mismatches.push("changedPaths");
151
+ if (scope.claimsFingerprint && scope.claimsFingerprint !== current.claimsFingerprint) mismatches.push("claimsFingerprint");
152
+ if (JSON.stringify(sortedPaths(scope.claimedPaths)) !== JSON.stringify(sortedPaths(current.claims.effectiveWriteClaims ?? []))) mismatches.push("claimedPaths");
153
+ if (scope.checkerCapabilityFingerprint !== undefined
154
+ && scope.checkerCapabilityFingerprint !== current.checkerCapabilities.fingerprint) mismatches.push("checkerCapabilityFingerprint");
155
+ if (mismatches.length > 0) {
156
+ throw scopeError("E_VERIFICATION_SCOPE_STALE", `Verification scope is stale; changed bindings: ${mismatches.join(", ")}`, [taskVerificationScopePath(taskId)]);
157
+ }
158
+ return { fresh: true, current };
159
+ }
160
+
161
+ export async function resolveVerificationScope(target, {
162
+ taskId,
163
+ mode = "AUTO",
164
+ packageRoot = getPackageRoot(),
165
+ createdAt = new Date().toISOString(),
166
+ } = {}) {
167
+ const requestedMode = normalizeMode(mode);
168
+ const inputs = await currentInputs(target, { taskId, packageRoot });
169
+ if (!inputs.state) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "Work state is required to resolve verification scope");
170
+ const changedPaths = inputs.changedPaths;
171
+ const claimedPaths = sortedPaths(inputs.claims.effectiveWriteClaims ?? []);
172
+ const hasTrustedScopedChecker = inputs.checkerCapabilities.valid && inputs.checkerCapabilities.checkers.length > 0;
173
+ const reasons = [];
174
+ let resolvedMode = requestedMode;
175
+ let selectedPaths = [];
176
+ let fallback = null;
177
+
178
+ const changedIsSafe = changedPaths !== null
179
+ && inputs.claims.valid
180
+ && changedPaths.every((item) => pathCovered(item, claimedPaths));
181
+ if (requiresFullVerification(inputs.contract)) {
182
+ if (requestedMode !== "FULL") reasons.push("The current contract explicitly requires full-project verification.");
183
+ resolvedMode = "FULL";
184
+ selectedPaths = [];
185
+ } else if (requestedMode === "FULL") {
186
+ resolvedMode = "FULL";
187
+ reasons.push("Full verification was explicitly requested.");
188
+ } else if (requestedMode === "CHANGED") {
189
+ if (!hasTrustedScopedChecker) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CHANGED scope requires a trusted scoped-checker capability");
190
+ if (changedPaths === null) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CHANGED scope cannot be proved because repository changes are unavailable");
191
+ if (!inputs.claims.valid || !changedIsSafe) {
192
+ throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CHANGED scope cannot be used because every changed path is not inside valid effective task claims");
193
+ }
194
+ resolvedMode = "CHANGED";
195
+ selectedPaths = changedPaths;
196
+ reasons.push("Selected exact canonical changed paths inside effective task claims.");
197
+ } else if (requestedMode === "CLAIMED") {
198
+ if (!hasTrustedScopedChecker) throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CLAIMED scope requires a trusted scoped-checker capability");
199
+ if (!inputs.claims.valid || claimedPaths.length === 0) {
200
+ throw scopeError("E_VERIFICATION_SCOPE_UNRESOLVED", "CLAIMED scope cannot be proved because effective task claims are unavailable");
201
+ }
202
+ resolvedMode = "CLAIMED";
203
+ selectedPaths = claimedPaths;
204
+ reasons.push("Selected the canonical effective task claims.");
205
+ } else if (hasTrustedScopedChecker && changedIsSafe) {
206
+ resolvedMode = "CHANGED";
207
+ selectedPaths = changedPaths;
208
+ reasons.push("AUTO selected exact changed paths because a trusted scoped checker exists and all paths are inside valid effective task claims.");
209
+ } else if (hasTrustedScopedChecker && inputs.claims.valid && claimedPaths.length > 0 && changedPaths !== null) {
210
+ resolvedMode = "CLAIMED";
211
+ selectedPaths = claimedPaths;
212
+ fallback = { from: "CHANGED", to: "CLAIMED", reason: "Changed paths could not be safely narrowed to the effective claims." };
213
+ reasons.push("AUTO escalated to effective claims because a narrower changed-path boundary was not provable.");
214
+ } else {
215
+ resolvedMode = "FULL";
216
+ selectedPaths = [];
217
+ fallback = {
218
+ from: "CHANGED",
219
+ to: "FULL",
220
+ reason: hasTrustedScopedChecker
221
+ ? "Repository changes or claim ownership could not be proved."
222
+ : "No trusted scoped-checker capability was established.",
223
+ };
224
+ reasons.push(hasTrustedScopedChecker
225
+ ? "AUTO escalated to full verification because a narrow boundary was not provable."
226
+ : "AUTO selected full verification because no trusted scoped-checker capability was established.");
227
+ }
228
+
229
+ const scope = await validateVerificationScope({
230
+ schemaVersion: 1,
231
+ protocolVersion: 1,
232
+ taskId,
233
+ requestedMode,
234
+ resolvedMode,
235
+ verificationCycle: inputs.state.verificationCycle ?? 1,
236
+ changedPaths: changedPaths ?? [],
237
+ claimedPaths,
238
+ selectedPaths,
239
+ reasons,
240
+ fallback,
241
+ contractFingerprint: inputs.contract.fingerprint,
242
+ repositoryFingerprint: inputs.repositoryFingerprint,
243
+ claimsFingerprint: inputs.claimsFingerprint,
244
+ ...(inputs.checkerCapabilities.fingerprint
245
+ ? { checkerCapabilityFingerprint: inputs.checkerCapabilities.fingerprint }
246
+ : {}),
247
+ createdAt,
248
+ }, packageRoot);
249
+ return { scope, inputs };
250
+ }
251
+
252
+ export async function persistVerificationScope(target, scope, { taskId, packageRoot = getPackageRoot() } = {}) {
253
+ const relativePath = taskVerificationScopePath(taskId);
254
+ const artifact = await writeJsonArtifact(target, relativePath, scope, "verification-scope", packageRoot, { taskId, operation: "verify-scope" });
255
+ await appendProtocolEvent(target, {
256
+ taskId,
257
+ event: "VERIFICATION_SCOPE_CAPTURED",
258
+ fingerprint: artifact.fingerprint,
259
+ details: { scopeFingerprint: artifact.fingerprint, resolvedMode: scope.resolvedMode, verificationCycle: scope.verificationCycle },
260
+ }, packageRoot, { taskId });
261
+ return { path: relativePath, fingerprint: artifact.fingerprint, scope };
262
+ }
263
+
264
+ export async function captureVerificationScope(target, options = {}) {
265
+ return withTaskTransaction({ target, taskId: options.taskId, packageRoot: options.packageRoot, operation: "verify-scope", recordCommitEvent: true }, async () => {
266
+ await assertTaskMutationAllowed(target, { taskId: options.taskId, packageRoot: options.packageRoot });
267
+ await assertWorkspaceBinding(target, { taskId: options.taskId, packageRoot: options.packageRoot, operation: "verify-scope" });
268
+ const result = await resolveVerificationScope(target, options);
269
+ return persistVerificationScope(target, result.scope, options);
270
+ });
271
+ }