@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.
- package/DOCS_INDEX.md +60 -0
- package/EXECUTION_STATE.md +9 -0
- package/LOOP_ENGINEERING.md +15 -0
- package/LOOP_SYSTEM_DESIGN.md +8 -0
- package/PROTOCOL_INTEGRATION.md +8 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +183 -654
- package/TERMINOLOGY.md +4 -0
- package/THREAT_MODEL.md +9 -0
- package/docs/assets/forgeloop-flow.svg +1 -0
- package/docs/forgeloop-flow.mmd +51 -0
- package/package.json +16 -3
- package/schemas/continuity.schema.json +57 -0
- package/scripts/CI_VALIDATORS.md +32 -0
- package/src/cli.js +307 -204
- package/src/commands/clear-continuity.js +9 -0
- package/src/commands/continuity.js +25 -0
- package/src/commands/doctor.js +17 -2
- package/src/commands/reconcile-continuity.js +23 -0
- package/src/commands/record-continuity.js +63 -0
- package/src/commands/status.js +14 -1
- package/src/commands/update.js +12 -13
- package/src/commands/validate-protocol.js +19 -0
- package/src/core/artifacts.js +1 -0
- package/src/core/bundles.js +6 -0
- package/src/core/command-resolution.js +295 -0
- package/src/core/command-tokenizer.js +122 -0
- package/src/core/conformance.js +8 -2
- package/src/core/continuity-cli-options.js +58 -0
- package/src/core/continuity-conformance.js +46 -0
- package/src/core/continuity-observability.js +20 -0
- package/src/core/continuity-reconciliation.js +224 -0
- package/src/core/continuity.js +245 -0
- package/src/core/inspect.js +9 -1
- package/src/core/installation-authority.js +178 -0
- package/src/core/json-safety.js +17 -13
- package/src/core/next-action-artifacts.js +118 -0
- package/src/core/next-action-continuity.js +65 -0
- package/src/core/next-action-model.js +127 -0
- package/src/core/next-action-phases.js +12 -0
- package/src/core/next-action.js +22 -249
- package/src/core/npm-classifier.js +343 -0
- package/src/core/package-manager-classifiers.js +37 -0
- package/src/core/preflight-consistency.js +221 -0
- package/src/core/preflight-loaders.js +112 -0
- package/src/core/preflight-model.js +83 -0
- package/src/core/preflight.js +34 -444
- package/src/core/protocol.js +7 -0
- package/src/core/schema-validation.js +15 -1
- package/src/core/templates.js +2 -0
- package/src/core/verification-capability.js +31 -1106
- 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
|
+
}
|
package/src/core/conformance.js
CHANGED
|
@@ -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
|
+
}
|