@cassiomc1/forgeloop 0.1.15 → 1.0.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.
Files changed (52) hide show
  1. package/DOCS_INDEX.md +60 -0
  2. package/EXECUTION_STATE.md +9 -0
  3. package/LOOP_ENGINEERING.md +15 -0
  4. package/LOOP_SYSTEM_DESIGN.md +8 -0
  5. package/PROTOCOL_INTEGRATION.md +8 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +183 -654
  8. package/TERMINOLOGY.md +4 -0
  9. package/THREAT_MODEL.md +9 -0
  10. package/docs/assets/forgeloop-flow.svg +1 -0
  11. package/docs/forgeloop-flow.mmd +51 -0
  12. package/package.json +16 -3
  13. package/schemas/continuity.schema.json +57 -0
  14. package/scripts/CI_VALIDATORS.md +32 -0
  15. package/src/cli.js +307 -204
  16. package/src/commands/clear-continuity.js +9 -0
  17. package/src/commands/continuity.js +25 -0
  18. package/src/commands/doctor.js +17 -2
  19. package/src/commands/reconcile-continuity.js +23 -0
  20. package/src/commands/record-continuity.js +63 -0
  21. package/src/commands/status.js +14 -1
  22. package/src/commands/update.js +12 -13
  23. package/src/commands/validate-protocol.js +19 -0
  24. package/src/core/artifacts.js +1 -0
  25. package/src/core/bundles.js +6 -0
  26. package/src/core/command-resolution.js +295 -0
  27. package/src/core/command-tokenizer.js +122 -0
  28. package/src/core/conformance.js +8 -2
  29. package/src/core/continuity-cli-options.js +58 -0
  30. package/src/core/continuity-conformance.js +46 -0
  31. package/src/core/continuity-observability.js +20 -0
  32. package/src/core/continuity-reconciliation.js +224 -0
  33. package/src/core/continuity.js +245 -0
  34. package/src/core/inspect.js +9 -1
  35. package/src/core/installation-authority.js +178 -0
  36. package/src/core/json-safety.js +17 -13
  37. package/src/core/next-action-artifacts.js +118 -0
  38. package/src/core/next-action-continuity.js +65 -0
  39. package/src/core/next-action-model.js +127 -0
  40. package/src/core/next-action-phases.js +12 -0
  41. package/src/core/next-action.js +22 -249
  42. package/src/core/npm-classifier.js +343 -0
  43. package/src/core/package-manager-classifiers.js +37 -0
  44. package/src/core/preflight-consistency.js +221 -0
  45. package/src/core/preflight-loaders.js +112 -0
  46. package/src/core/preflight-model.js +83 -0
  47. package/src/core/preflight.js +34 -444
  48. package/src/core/protocol.js +7 -0
  49. package/src/core/schema-validation.js +15 -1
  50. package/src/core/templates.js +2 -0
  51. package/src/core/verification-capability.js +31 -1106
  52. package/src/core/verification-constants.js +61 -0
@@ -0,0 +1,122 @@
1
+ export function tokenizeCommand(commandString) {
2
+ const tokens = [];
3
+ let current = "";
4
+ let inSingleQuote = false;
5
+ let inDoubleQuote = false;
6
+
7
+ for (let i = 0; i < commandString.length; i++) {
8
+ const char = commandString[i];
9
+
10
+ if (char === "'" && !inDoubleQuote) {
11
+ inSingleQuote = !inSingleQuote;
12
+ } else if (char === '"' && !inSingleQuote) {
13
+ inDoubleQuote = !inDoubleQuote;
14
+ } else if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) {
15
+ if (current.length > 0) {
16
+ tokens.push(current);
17
+ current = "";
18
+ }
19
+ } else {
20
+ current += char;
21
+ }
22
+ }
23
+
24
+ if (current.length > 0) tokens.push(current);
25
+ return tokens;
26
+ }
27
+
28
+ export function splitCommandPipeline(commandString) {
29
+ const parts = [];
30
+ let current = "";
31
+ let inSingleQuote = false;
32
+ let inDoubleQuote = false;
33
+
34
+ for (let i = 0; i < commandString.length; i++) {
35
+ const char = commandString[i];
36
+ const next = commandString[i + 1];
37
+
38
+ if (char === "'" && !inDoubleQuote) {
39
+ inSingleQuote = !inSingleQuote;
40
+ current += char;
41
+ } else if (char === '"' && !inSingleQuote) {
42
+ inDoubleQuote = !inDoubleQuote;
43
+ current += char;
44
+ } else if (!inSingleQuote && !inDoubleQuote) {
45
+ if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
46
+ if (current.trim().length > 0) parts.push(current.trim());
47
+ current = "";
48
+ i++;
49
+ } else if (char === ";" || char === "|") {
50
+ if (current.trim().length > 0) parts.push(current.trim());
51
+ current = "";
52
+ } else {
53
+ current += char;
54
+ }
55
+ } else {
56
+ current += char;
57
+ }
58
+ }
59
+
60
+ if (current.trim().length > 0) parts.push(current.trim());
61
+ return parts.length > 0 ? parts : [commandString];
62
+ }
63
+
64
+ export function extractToolFromArgs(args) {
65
+ if (!Array.isArray(args) || args.length === 0) return null;
66
+ for (let idx = 0; idx < args.length; idx++) {
67
+ const arg = args[idx];
68
+ if (arg === "-p" || arg === "--package") {
69
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
70
+ }
71
+ if (arg.startsWith("--package=")) return arg.split("=")[1];
72
+ if (!arg.startsWith("-")) return arg;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export function extractNpmExecTool(args) {
78
+ if (!Array.isArray(args) || args.length === 0) return null;
79
+ for (let idx = 0; idx < args.length; idx++) {
80
+ const arg = args[idx];
81
+ if (arg === "-p" || arg === "--package") {
82
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
83
+ }
84
+ if (arg.startsWith("--package=")) return arg.slice("--package=".length);
85
+ }
86
+ let afterDoubleDash = false;
87
+ for (let idx = 0; idx < args.length; idx++) {
88
+ const arg = args[idx];
89
+ if (arg === "--") {
90
+ afterDoubleDash = true;
91
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
92
+ continue;
93
+ }
94
+ if (!afterDoubleDash && !arg.startsWith("-")) return arg;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ export function normalizeExecutableName(binaryToken) {
100
+ const base = binaryToken.split(/[\\/]/u).pop() ?? binaryToken;
101
+ return base.toLowerCase().replace(/\.(?:cmd|bat|exe)$/u, "");
102
+ }
103
+
104
+ export function unwrapCommandArgv(argv) {
105
+ if (!Array.isArray(argv) || argv.length === 0) return null;
106
+ let i = 0;
107
+ while (i < argv.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(argv[i])) i++;
108
+ if (i >= argv.length) return null;
109
+ const binary = normalizeExecutableName(argv[i]);
110
+ if (["sh", "bash", "zsh", "dash", "ksh"].includes(binary)) {
111
+ const shellFlagIndex = argv.findIndex((item, index) => index > i && /^-.*c/.test(item));
112
+ const shellCommand = shellFlagIndex >= 0 ? argv[shellFlagIndex + 1] : null;
113
+ if (shellCommand) return tokenizeCommand(shellCommand);
114
+ }
115
+ if (binary === "cmd") {
116
+ const shellFlagIndex = argv.findIndex((item, index) => index > i && /^\/c$/iu.test(item));
117
+ const shellCommand = shellFlagIndex >= 0 ? argv.slice(shellFlagIndex + 1).join(" ") : null;
118
+ if (shellCommand) return tokenizeCommand(shellCommand);
119
+ }
120
+ if (binary === "call") return argv.slice(i + 1);
121
+ return argv.slice(i);
122
+ }
@@ -1,6 +1,7 @@
1
1
  import { PROTOCOL_VERSION } from "./protocol.js";
2
2
  import { createEvidence } from "./evidence.js";
3
3
  import { canonicalFingerprint } from "./artifacts.js";
4
+ import { evaluateContinuityConformance } from "./continuity-conformance.js";
4
5
 
5
6
  function error(code, message, artifacts = []) {
6
7
  return { code, message, artifacts };
@@ -49,9 +50,13 @@ export function validateTaskArtifactSet({
49
50
  taskBriefs = [],
50
51
  delegatedResults = [],
51
52
  events = [],
53
+ continuity = null,
54
+ continuityContext = {},
52
55
  } = {}) {
53
56
  const errors = [];
54
57
  const incomplete = [];
58
+ const continuityResult = evaluateContinuityConformance({ continuity, state, ...continuityContext });
59
+ errors.push(...continuityResult.errors);
55
60
 
56
61
  addVersionErrors(route, "route", errors);
57
62
  addVersionErrors(state, "state", errors);
@@ -122,11 +127,11 @@ export function validateTaskArtifactSet({
122
127
 
123
128
  const sortedErrors = sortErrors(errors);
124
129
  let status = "VALID";
125
- if (sortedErrors.some((item) => item.code === "UNSUPPORTED_PROTOCOL_VERSION")) {
130
+ if (sortedErrors.some((item) => item.code === "UNSUPPORTED_PROTOCOL_VERSION") || continuityResult.status === "INVALID") {
126
131
  status = "INVALID";
127
132
  } else if (sortedErrors.length > 0) {
128
133
  status = "INCONSISTENT";
129
- } else if (stateClassification?.status === "REVALIDATION_REQUIRED") {
134
+ } else if (stateClassification?.status === "REVALIDATION_REQUIRED" || continuityResult.status === "STALE") {
130
135
  status = "STALE";
131
136
  } else if (incomplete.length > 0) {
132
137
  status = "INCOMPLETE";
@@ -173,6 +178,7 @@ export function validateTaskArtifactSet({
173
178
  incomplete: [...new Set(incomplete)].sort(),
174
179
  stale,
175
180
  delegation,
181
+ continuity: continuityResult,
176
182
  evidence: [createEvidence({
177
183
  kind: evidenceKind,
178
184
  source: "ForgeLoop protocol conformance",
@@ -0,0 +1,58 @@
1
+ const OPTION_FIELDS = Object.freeze({
2
+ "--focus-id": ["continuityFocusId", false],
3
+ "--focus-summary": ["continuityFocusSummary", false],
4
+ "--remaining": ["continuityRemaining", true],
5
+ "--known-issue": ["continuityKnownIssues", true],
6
+ "--changed-area": ["continuityChangedAreas", true],
7
+ "--inspect-first": ["continuityInspectFirst", true],
8
+ "--resume-note": ["continuityResumeNote", false],
9
+ });
10
+
11
+ export function continuityOptionDefaults() {
12
+ return {
13
+ continuityFocusId: null,
14
+ continuityFocusSummary: null,
15
+ continuityRemaining: [],
16
+ continuityKnownIssues: [],
17
+ continuityChangedAreas: [],
18
+ continuityInspectFirst: [],
19
+ continuityResumeNote: null,
20
+ };
21
+ }
22
+
23
+ export function consumeContinuityOption({ argument, argv, index, options }) {
24
+ const definition = OPTION_FIELDS[argument];
25
+ if (!definition) return { handled: false, index };
26
+ const value = argv[index + 1];
27
+ if (!value || value.startsWith("-")) throw new Error(`${argument} requires a value`);
28
+ const [field, repeatable] = definition;
29
+ if (repeatable) options[field].push(value);
30
+ else options[field] = value;
31
+ return { handled: true, index: index + 1 };
32
+ }
33
+
34
+ export function hasContinuityOptions(options = {}) {
35
+ return Boolean(
36
+ options.continuityFocusId
37
+ || options.continuityFocusSummary
38
+ || options.continuityResumeNote
39
+ || options.continuityRemaining?.length
40
+ || options.continuityKnownIssues?.length
41
+ || options.continuityChangedAreas?.length
42
+ || options.continuityInspectFirst?.length
43
+ );
44
+ }
45
+
46
+ export function validateContinuityOptions(command, options = {}) {
47
+ if (command !== "record-continuity" && hasContinuityOptions(options)) {
48
+ throw new Error(`Continuity recording options are not valid for ${command}`);
49
+ }
50
+ if (command === "record-continuity") {
51
+ const hasFocusId = Boolean(options.continuityFocusId);
52
+ const hasFocusSummary = Boolean(options.continuityFocusSummary);
53
+ if (hasFocusId !== hasFocusSummary) {
54
+ throw new Error("record-continuity requires --focus-id and --focus-summary together");
55
+ }
56
+ }
57
+ return options;
58
+ }
@@ -0,0 +1,46 @@
1
+ import { classifyContinuity } from "./continuity-reconciliation.js";
2
+
3
+ function error(code, message) {
4
+ return { code, message, artifacts: ["continuity", "state"] };
5
+ }
6
+
7
+ export function evaluateContinuityConformance(input = {}) {
8
+ if (!input.continuity) {
9
+ return {
10
+ required: false,
11
+ status: "NOT_APPLICABLE",
12
+ classification: "ABSENT",
13
+ errors: [],
14
+ reasonCodes: [],
15
+ reasons: ["CONTINUITY_ABSENT"],
16
+ authority: "OPERATIONAL_CONTEXT_ONLY",
17
+ evidenceAuthority: "NONE",
18
+ };
19
+ }
20
+
21
+ const classification = classifyContinuity(input);
22
+ const result = {
23
+ required: false,
24
+ status: classification.classification === "FRESH"
25
+ ? "VALID"
26
+ : classification.classification === "RECONCILIATION_REQUIRED"
27
+ ? "STALE"
28
+ : classification.classification === "NOT_APPLICABLE"
29
+ ? "NOT_APPLICABLE"
30
+ : classification.classification,
31
+ classification: classification.classification,
32
+ errors: [],
33
+ reasonCodes: [...classification.reasonCodes],
34
+ reasons: [...classification.reasons],
35
+ authority: "OPERATIONAL_CONTEXT_ONLY",
36
+ evidenceAuthority: "NONE",
37
+ };
38
+
39
+ if (["INVALID", "INCONSISTENT"].includes(classification.classification)) {
40
+ result.errors = classification.reasonCodes.map((code, index) => error(
41
+ code,
42
+ classification.reasons[index] ?? `Continuity is ${classification.classification.toLowerCase()}`,
43
+ ));
44
+ }
45
+ return result;
46
+ }
@@ -0,0 +1,20 @@
1
+ const HEALTHY = new Set(["ABSENT", "FRESH", "NOT_APPLICABLE"]);
2
+
3
+ export function continuityIsHealthy(result) {
4
+ return HEALTHY.has(result?.classification ?? "ABSENT");
5
+ }
6
+
7
+ export function continuityFinding(result) {
8
+ if (continuityIsHealthy(result)) return null;
9
+ const classification = result?.classification ?? "INVALID";
10
+ const severity = classification === "RECONCILIATION_REQUIRED" ? "warning" : "error";
11
+ return {
12
+ code: `continuity-${classification.toLowerCase().replaceAll("_", "-")}`,
13
+ severity,
14
+ path: result?.path ?? ".forgeloop/continuity.json",
15
+ message: result?.reasons?.join(", ") || `Continuity is ${classification}`,
16
+ remediation: classification === "RECONCILIATION_REQUIRED"
17
+ ? "Run forgeloop reconcile-continuity, inspect the current checkout, then record corrected continuity before advancing verification."
18
+ : "Repair or clear continuity after reviewing the current work state and checkout.",
19
+ };
20
+ }
@@ -0,0 +1,224 @@
1
+ import { canonicalFingerprint } from "./artifacts.js";
2
+ import { assertContinuitySemantics, readContinuity } from "./continuity.js";
3
+ import { WORK_TRANSITIONS } from "./protocol.js";
4
+
5
+ const RECONCILIATION_CODE = "E_CONTINUITY_RECONCILIATION_REQUIRED";
6
+
7
+ function uniqueSorted(values) {
8
+ return [...new Set(values.filter(Boolean))].sort();
9
+ }
10
+
11
+ function pathCoveredByHint(changedPath, hint) {
12
+ const normalizedPath = String(changedPath).replaceAll("\\", "/").replace(/^\.\//, "");
13
+ const normalizedHint = String(hint).replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
14
+ return normalizedPath === normalizedHint || normalizedPath.startsWith(`${normalizedHint}/`);
15
+ }
16
+
17
+ function compareChangedPaths(changedPaths, hints) {
18
+ if (!Array.isArray(changedPaths)) return "NOT_VERIFIED";
19
+ if (!Array.isArray(hints) || hints.length === 0) return changedPaths.length === 0 ? "MATCH" : "MISMATCH";
20
+ const matched = hints.filter((hint) => changedPaths.some((changedPath) => pathCoveredByHint(changedPath, hint))).length;
21
+ if (matched === hints.length) return "MATCH";
22
+ if (matched > 0) return "PARTIAL";
23
+ return "MISMATCH";
24
+ }
25
+
26
+ function compareRepository(saved, current) {
27
+ const savedUnavailable = !saved || (saved.branch === null && saved.head === null);
28
+ const currentUnavailable = !current || (current.branch === null && current.head === null);
29
+ if (savedUnavailable && currentUnavailable) return "NOT_VERIFIED";
30
+ if (!saved || !current) return "NOT_VERIFIED";
31
+ return saved.branch === current.branch && saved.head === current.head ? "MATCH" : "MISMATCH";
32
+ }
33
+
34
+ function phaseReachable(from, to) {
35
+ if (from === to) return true;
36
+ if (to === "BLOCKED" && from !== "COMPLETE") return true;
37
+ if (from === "BLOCKED") return false;
38
+ const queue = [from];
39
+ const visited = new Set(queue);
40
+ while (queue.length > 0) {
41
+ const phase = queue.shift();
42
+ for (const next of WORK_TRANSITIONS[phase] ?? []) {
43
+ if (next === to) return true;
44
+ if (!visited.has(next)) {
45
+ visited.add(next);
46
+ queue.push(next);
47
+ }
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ function baseResult(classification, overrides = {}) {
54
+ return {
55
+ classification,
56
+ taskMatches: overrides.taskMatches ?? null,
57
+ workStateMatches: overrides.workStateMatches ?? null,
58
+ contractMatches: overrides.contractMatches ?? null,
59
+ phaseMatches: overrides.phaseMatches ?? null,
60
+ repositoryComparison: overrides.repositoryComparison ?? "NOT_VERIFIED",
61
+ changedPathComparison: overrides.changedPathComparison ?? "NOT_VERIFIED",
62
+ reasonCodes: uniqueSorted(overrides.reasonCodes ?? []),
63
+ reasons: uniqueSorted(overrides.reasons ?? []),
64
+ authority: "OPERATIONAL_CONTEXT_ONLY",
65
+ evidenceAuthority: "NONE",
66
+ };
67
+ }
68
+
69
+ export function classifyContinuity({
70
+ continuity,
71
+ state,
72
+ contractFingerprint,
73
+ repositoryFingerprint,
74
+ changedPaths,
75
+ } = {}) {
76
+ if (!continuity) {
77
+ return baseResult("ABSENT", { reasons: ["CONTINUITY_ABSENT"] });
78
+ }
79
+
80
+ let value;
81
+ try {
82
+ value = assertContinuitySemantics(continuity);
83
+ } catch (error) {
84
+ return baseResult("INVALID", {
85
+ reasonCodes: [error.code ?? "E_CONTINUITY_INVALID"],
86
+ reasons: [error.message],
87
+ });
88
+ }
89
+
90
+ if (!state || typeof state !== "object") {
91
+ return baseResult("INCONSISTENT", {
92
+ reasonCodes: ["E_CONTINUITY_STATE_MISSING"],
93
+ reasons: ["CONTINUITY_STATE_MISSING"],
94
+ });
95
+ }
96
+
97
+ if (state.phase === "COMPLETE") {
98
+ return baseResult("NOT_APPLICABLE", {
99
+ taskMatches: value.taskId === state.taskId,
100
+ phaseMatches: value.phase === state.phase,
101
+ reasons: ["CONTINUITY_NOT_APPLICABLE_AFTER_COMPLETE"],
102
+ });
103
+ }
104
+
105
+ const taskMatches = value.taskId === state.taskId;
106
+ const workStateMatches = value.workStateFingerprint === canonicalFingerprint(state);
107
+ const expectedContract = contractFingerprint ?? state.contractFingerprint ?? null;
108
+ const contractMatches = value.contractFingerprint === state.contractFingerprint
109
+ && (expectedContract === null || value.contractFingerprint === expectedContract);
110
+ const phaseMatches = value.phase === state.phase;
111
+ const repositoryComparison = compareRepository(value.repositoryFingerprint, repositoryFingerprint);
112
+ const changedPathComparison = compareChangedPaths(changedPaths, value.changedAreas);
113
+
114
+ const inconsistentCodes = [];
115
+ const inconsistentReasons = [];
116
+ if (!taskMatches) {
117
+ inconsistentCodes.push("E_CONTINUITY_TASK_MISMATCH");
118
+ inconsistentReasons.push("CONTINUITY_TASK_MISMATCH");
119
+ }
120
+ if (!contractMatches) {
121
+ inconsistentCodes.push("E_CONTINUITY_CONTRACT_MISMATCH");
122
+ inconsistentReasons.push("CONTINUITY_CONTRACT_MISMATCH");
123
+ }
124
+ if (!phaseMatches && !phaseReachable(value.phase, state.phase)) {
125
+ inconsistentCodes.push("E_CONTINUITY_PHASE_MISMATCH");
126
+ inconsistentReasons.push("CONTINUITY_PHASE_MISMATCH");
127
+ }
128
+ if (inconsistentCodes.length > 0) {
129
+ return baseResult("INCONSISTENT", {
130
+ taskMatches,
131
+ workStateMatches,
132
+ contractMatches,
133
+ phaseMatches,
134
+ repositoryComparison,
135
+ changedPathComparison,
136
+ reasonCodes: inconsistentCodes,
137
+ reasons: inconsistentReasons,
138
+ });
139
+ }
140
+
141
+ const reconciliationReasons = [];
142
+ if (!workStateMatches) reconciliationReasons.push("CONTINUITY_WORK_STATE_CHANGED");
143
+ if (!phaseMatches) reconciliationReasons.push("CONTINUITY_PHASE_CHANGED");
144
+ if (repositoryComparison === "MISMATCH") reconciliationReasons.push("CONTINUITY_REPOSITORY_CHANGED");
145
+ if (["PARTIAL", "MISMATCH"].includes(changedPathComparison)) {
146
+ reconciliationReasons.push("CONTINUITY_CHANGED_PATHS_DIFFER");
147
+ }
148
+
149
+ if (reconciliationReasons.length > 0) {
150
+ return baseResult("RECONCILIATION_REQUIRED", {
151
+ taskMatches,
152
+ workStateMatches,
153
+ contractMatches,
154
+ phaseMatches,
155
+ repositoryComparison,
156
+ changedPathComparison,
157
+ reasonCodes: [RECONCILIATION_CODE],
158
+ reasons: reconciliationReasons,
159
+ });
160
+ }
161
+
162
+ return baseResult("FRESH", {
163
+ taskMatches,
164
+ workStateMatches,
165
+ contractMatches,
166
+ phaseMatches,
167
+ repositoryComparison,
168
+ changedPathComparison,
169
+ });
170
+ }
171
+
172
+ export async function reconcileContinuity({ target, packageRoot } = {}) {
173
+ const [{ readWorkState }, { readContract }, repository] = await Promise.all([
174
+ import("./work-state.js"),
175
+ import("./contract.js"),
176
+ import("./repository.js"),
177
+ ]);
178
+
179
+ const state = await readWorkState(target, packageRoot);
180
+ let continuityArtifact;
181
+ try {
182
+ continuityArtifact = await readContinuity(target, packageRoot);
183
+ } catch (error) {
184
+ if (error.code === "ARTIFACT_MISSING") {
185
+ return { ...classifyContinuity({ continuity: null, state }), path: ".forgeloop/continuity.json", present: false };
186
+ }
187
+ return {
188
+ ...baseResult("INVALID", {
189
+ reasonCodes: [error.code ?? "E_CONTINUITY_INVALID"],
190
+ reasons: [error.message],
191
+ }),
192
+ path: ".forgeloop/continuity.json",
193
+ present: true,
194
+ error: error.message,
195
+ };
196
+ }
197
+
198
+ let contractFingerprint = null;
199
+ try {
200
+ const contract = await readContract(target, packageRoot);
201
+ contractFingerprint = contract.fingerprint;
202
+ } catch {
203
+ contractFingerprint = null;
204
+ }
205
+
206
+ const [repositoryFingerprint, changedPaths] = await Promise.all([
207
+ repository.currentRepositoryFingerprint(target),
208
+ repository.currentChangedPaths(target),
209
+ ]);
210
+
211
+ return {
212
+ ...classifyContinuity({
213
+ continuity: continuityArtifact.value,
214
+ state,
215
+ contractFingerprint,
216
+ repositoryFingerprint,
217
+ changedPaths,
218
+ }),
219
+ path: continuityArtifact.path,
220
+ present: true,
221
+ fingerprint: continuityArtifact.fingerprint,
222
+ continuity: continuityArtifact.value,
223
+ };
224
+ }