@cassiomc1/forgeloop 0.1.14 → 0.1.16

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 (61) hide show
  1. package/.forgeloop/forgeloop.gitignore +1 -0
  2. package/DOCS_INDEX.md +60 -0
  3. package/EXECUTION_STATE.md +9 -0
  4. package/LOOP_ENGINEERING.md +44 -6
  5. package/LOOP_SYSTEM_DESIGN.md +8 -0
  6. package/PROTOCOL_INTEGRATION.md +41 -0
  7. package/QUALITY_SCORECARD.md +2 -0
  8. package/README.md +183 -643
  9. package/TERMINOLOGY.md +4 -0
  10. package/THREAT_MODEL.md +14 -0
  11. package/docs/assets/forgeloop-flow.svg +1 -0
  12. package/docs/forgeloop-flow.mmd +51 -0
  13. package/package.json +16 -3
  14. package/schemas/check.schema.json +2 -0
  15. package/schemas/continuity.schema.json +57 -0
  16. package/schemas/execution.schema.json +63 -0
  17. package/scripts/CI_VALIDATORS.md +32 -0
  18. package/src/cli.js +351 -196
  19. package/src/commands/clear-continuity.js +9 -0
  20. package/src/commands/continuity.js +25 -0
  21. package/src/commands/doctor.js +17 -2
  22. package/src/commands/reconcile-continuity.js +23 -0
  23. package/src/commands/record-continuity.js +63 -0
  24. package/src/commands/run-check.js +83 -0
  25. package/src/commands/status.js +14 -1
  26. package/src/commands/update.js +12 -13
  27. package/src/commands/validate-protocol.js +37 -0
  28. package/src/core/artifacts.js +9 -0
  29. package/src/core/bundles.js +78 -0
  30. package/src/core/checks.js +16 -0
  31. package/src/core/command-resolution.js +295 -0
  32. package/src/core/command-tokenizer.js +122 -0
  33. package/src/core/completion-artifacts.js +229 -47
  34. package/src/core/completion.js +19 -1
  35. package/src/core/conformance.js +8 -2
  36. package/src/core/continuity-cli-options.js +58 -0
  37. package/src/core/continuity-conformance.js +46 -0
  38. package/src/core/continuity-observability.js +20 -0
  39. package/src/core/continuity-reconciliation.js +224 -0
  40. package/src/core/continuity.js +245 -0
  41. package/src/core/evidence-readiness.js +26 -1
  42. package/src/core/execution.js +185 -0
  43. package/src/core/inspect.js +9 -1
  44. package/src/core/installation-authority.js +178 -0
  45. package/src/core/json-safety.js +17 -13
  46. package/src/core/next-action-artifacts.js +118 -0
  47. package/src/core/next-action-continuity.js +65 -0
  48. package/src/core/next-action-model.js +127 -0
  49. package/src/core/next-action-phases.js +12 -0
  50. package/src/core/next-action.js +22 -249
  51. package/src/core/npm-classifier.js +343 -0
  52. package/src/core/package-manager-classifiers.js +37 -0
  53. package/src/core/preflight-consistency.js +221 -0
  54. package/src/core/preflight-loaders.js +112 -0
  55. package/src/core/preflight-model.js +83 -0
  56. package/src/core/preflight.js +34 -444
  57. package/src/core/protocol.js +7 -0
  58. package/src/core/schema-validation.js +16 -1
  59. package/src/core/templates.js +3 -0
  60. package/src/core/verification-capability.js +31 -495
  61. package/src/core/verification-constants.js +61 -0
@@ -8,7 +8,7 @@ import {
8
8
  import { readContract } from "./contract.js";
9
9
  import { appendProtocolEvent, validateEventLedger } from "./events.js";
10
10
  import { completionEvidenceForGuides } from "./guide-metadata.js";
11
- import { createCheck } from "./checks.js";
11
+ import { CHECK_PROVENANCE, createCheck } from "./checks.js";
12
12
  import { createEvidence } from "./evidence.js";
13
13
  import { coverageForRequirements } from "./coverage.js";
14
14
  import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
@@ -20,6 +20,7 @@ import { readWorkState, writeWorkState } from "./work-state.js";
20
20
  import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
21
21
  import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
22
22
  import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
23
+ import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
23
24
 
24
25
  function artifactError(code, message, artifacts = []) {
25
26
  const error = new Error(message);
@@ -59,6 +60,102 @@ async function readOptionalConfig(target, packageRoot) {
59
60
  }
60
61
  }
61
62
 
63
+ function formatArgv(argv) {
64
+ return argv.map((argument) => /[\s"']/u.test(argument)
65
+ ? JSON.stringify(argument)
66
+ : argument).join(" ");
67
+ }
68
+
69
+ function commandProvenanceError(message = "Observed command evidence requires a ForgeLoop execution artifact") {
70
+ return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
71
+ }
72
+
73
+ /**
74
+ * Revalidate the ForgeLoop-owned execution artifact behind an observed command
75
+ * check. This is intentionally asynchronous so completion and audit can verify
76
+ * the artifact instead of trusting duplicated check metadata.
77
+ */
78
+ export async function validateCheckExecutionProvenance(check, {
79
+ target,
80
+ packageRoot,
81
+ taskId,
82
+ executionArtifacts,
83
+ allowForeignCwd = false,
84
+ } = {}) {
85
+ if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") return null;
86
+ if (check.provenance !== "FORGELOOP_EXECUTED" || !check.executionRef) {
87
+ throw commandProvenanceError();
88
+ }
89
+ const artifact = executionArtifacts
90
+ ? { value: executionArtifacts[check.executionRef] }
91
+ : await readExecutionArtifact({
92
+ target,
93
+ executionRef: check.executionRef,
94
+ packageRoot,
95
+ });
96
+ if (!artifact.value) {
97
+ throw artifactError(
98
+ "E_EXECUTION_REF_INVALID",
99
+ "Execution reference does not resolve to an execution artifact in this bundle",
100
+ [ARTIFACT_PATHS.executionDirectory],
101
+ );
102
+ }
103
+ const execution = validateExecutionBinding({
104
+ execution: artifact.value,
105
+ taskId,
106
+ checkId: check.id,
107
+ requirement: check.requirement,
108
+ verificationCycle: check.details?.verificationCycle ?? 1,
109
+ });
110
+ if (!allowForeignCwd && path.resolve(execution.cwd) !== path.resolve(target)) {
111
+ throw artifactError(
112
+ "E_EXECUTION_REF_INVALID",
113
+ "Execution artifact cwd does not match the current target",
114
+ [ARTIFACT_PATHS.executionDirectory],
115
+ );
116
+ }
117
+ if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
118
+ throw artifactError(
119
+ "E_EXECUTION_REF_INVALID",
120
+ "A passed command check must reference a successful execution artifact",
121
+ [ARTIFACT_PATHS.executionDirectory],
122
+ );
123
+ }
124
+ if (check.status === "failed" && execution.status !== "failed") {
125
+ throw artifactError(
126
+ "E_EXECUTION_REF_INVALID",
127
+ "A failed command check must reference a failed execution artifact",
128
+ [ARTIFACT_PATHS.executionDirectory],
129
+ );
130
+ }
131
+ if (check.exitCode !== undefined && check.exitCode !== execution.exitCode) {
132
+ throw artifactError(
133
+ "E_EXECUTION_REF_INVALID",
134
+ "Check exitCode does not match its execution artifact",
135
+ [ARTIFACT_PATHS.executionDirectory],
136
+ );
137
+ }
138
+ return execution;
139
+ }
140
+
141
+ export async function validateChecksExecutionProvenance(checks, options = {}) {
142
+ const errors = [];
143
+ for (const check of Array.isArray(checks) ? checks : []) {
144
+ try {
145
+ await validateCheckExecutionProvenance(check, options);
146
+ } catch (error) {
147
+ errors.push({
148
+ code: error.code ?? "E_EXECUTION_REF_INVALID",
149
+ message: error.message,
150
+ artifacts: error.artifacts ?? [options.artifactPath ?? ARTIFACT_PATHS.receipt],
151
+ checkId: check?.id,
152
+ requirementId: check?.requirement,
153
+ });
154
+ }
155
+ }
156
+ return errors;
157
+ }
158
+
62
159
  export async function requiredEvidenceForTarget({ target, contract, route, packageRoot, additionalEvidence = [] }) {
63
160
  const config = await readOptionalConfig(target, packageRoot);
64
161
  const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
@@ -196,42 +293,20 @@ function appendUniqueEvidence(evidence, nextEvidence) {
196
293
  return exists ? [...evidence] : [...evidence, nextEvidence];
197
294
  }
198
295
 
199
- export async function recordCheck({
296
+ /**
297
+ * Read-only lifecycle checks shared by run-check and record-check. Keeping
298
+ * these checks before process launch prevents a command from running when the
299
+ * target is not ready to receive verification evidence.
300
+ */
301
+ export async function assertRecordCheckPrerequisites({
200
302
  target,
201
303
  packageRoot,
202
- id,
203
- kind = "command",
204
304
  requirement,
205
305
  status,
206
306
  evidenceKind,
207
- command,
208
- result,
209
- exitCode,
210
- details,
211
307
  authorityContext,
212
308
  runtimeContext,
213
- }) {
214
- requiredString(id, "check id");
215
- requiredString(kind, "check kind");
216
- requiredString(requirement, "check requirement");
217
- requiredString(status, "check status");
218
- requiredString(evidenceKind, "evidence kind");
219
- if (command !== undefined && typeof command !== "string") {
220
- throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
221
- }
222
- if (result !== undefined && typeof result !== "string") {
223
- throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
224
- }
225
- if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
226
- throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
227
- }
228
- if ((typeof command !== "string" || command.trim() === "")
229
- && (typeof result !== "string" || result.trim() === "")) {
230
- throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
231
- }
232
-
233
- const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
234
-
309
+ } = {}) {
235
310
  const state = await readWorkState(target, packageRoot);
236
311
  if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
237
312
  if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
@@ -274,9 +349,114 @@ export async function recordCheck({
274
349
  authorityContext,
275
350
  runtimeContext,
276
351
  });
277
- const source = commandSpec || `check:${id}`;
278
- const recordedResult = result?.trim() || `recorded command: ${commandSpec || source}`;
279
- const classification = commandSpec !== undefined ? classifyCommandResolution(commandSpec) : null;
352
+ const ledger = await validateEventLedger(target, packageRoot);
353
+ if (!ledger.valid) {
354
+ const first = ledger.errors[0];
355
+ throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
356
+ }
357
+ if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
358
+ throw artifactError(
359
+ "E_PHASE_CHRONOLOGY_INVALID",
360
+ "record-check requires VERIFICATION_STARTED in the current task ledger",
361
+ [ARTIFACT_PATHS.events],
362
+ );
363
+ }
364
+ return {
365
+ state,
366
+ contract,
367
+ route,
368
+ preflight,
369
+ requiredEvidence,
370
+ existingReceipt,
371
+ ledger,
372
+ };
373
+ }
374
+
375
+ export async function recordCheck({
376
+ target,
377
+ packageRoot,
378
+ id,
379
+ kind = "command",
380
+ requirement,
381
+ status,
382
+ evidenceKind,
383
+ command,
384
+ result,
385
+ exitCode,
386
+ details,
387
+ executionRef,
388
+ provenance,
389
+ authorityContext,
390
+ runtimeContext,
391
+ }) {
392
+ requiredString(id, "check id");
393
+ requiredString(kind, "check kind");
394
+ requiredString(requirement, "check requirement");
395
+ requiredString(status, "check status");
396
+ requiredString(evidenceKind, "evidence kind");
397
+ if (command !== undefined && typeof command !== "string") {
398
+ throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
399
+ }
400
+ if (result !== undefined && typeof result !== "string") {
401
+ throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
402
+ }
403
+ if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
404
+ throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
405
+ }
406
+ if (executionRef !== undefined && (typeof executionRef !== "string" || executionRef.trim() === "")) {
407
+ throw artifactError("E_EXECUTION_REF_INVALID", "executionRef must be a non-empty string when supplied");
408
+ }
409
+ if (provenance !== undefined && !CHECK_PROVENANCE.includes(provenance)) {
410
+ throw artifactError("E_CHECK_INVALID", `provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
411
+ }
412
+ if ((typeof command !== "string" || command.trim() === "")
413
+ && (typeof result !== "string" || result.trim() === "")) {
414
+ throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
415
+ }
416
+
417
+ const context = await assertRecordCheckPrerequisites({
418
+ target,
419
+ packageRoot,
420
+ requirement,
421
+ status,
422
+ evidenceKind,
423
+ authorityContext,
424
+ runtimeContext,
425
+ });
426
+ const {
427
+ state,
428
+ contract,
429
+ route,
430
+ requiredEvidence,
431
+ existingReceipt,
432
+ } = context;
433
+
434
+ const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
435
+ const observedCommand = kind === "command" && evidenceKind === "OBSERVED";
436
+ if (observedCommand && (!executionRef || provenance !== "FORGELOOP_EXECUTED")) {
437
+ throw commandProvenanceError();
438
+ }
439
+ const execution = executionRef
440
+ ? await validateCheckExecutionProvenance({
441
+ kind,
442
+ evidenceKind,
443
+ provenance,
444
+ executionRef,
445
+ status,
446
+ exitCode,
447
+ id,
448
+ requirement,
449
+ details: { ...(details ?? {}), verificationCycle: state.verificationCycle ?? 1 },
450
+ }, {
451
+ target,
452
+ packageRoot,
453
+ taskId: contract.value.taskId,
454
+ })
455
+ : null;
456
+ const effectiveCommand = execution ? formatArgv(execution.argv) : commandSpec;
457
+ const source = effectiveCommand || `check:${id}`;
458
+ const recordedResult = result?.trim() || `recorded command: ${effectiveCommand || source}`;
459
+ const classification = execution?.resolution ?? (effectiveCommand !== undefined ? classifyCommandResolution(effectiveCommand) : null);
280
460
  const installationAuthorized = Boolean(
281
461
  details?.installationAuthorized
282
462
  || details?.authority?.softwareInstallation === "AUTHORIZED"
@@ -289,15 +469,28 @@ export async function recordCheck({
289
469
  status,
290
470
  evidenceKind,
291
471
  source,
472
+ ...(executionRef === undefined ? {} : { executionRef }),
473
+ ...(provenance === undefined ? {} : { provenance }),
292
474
  timestamp: new Date().toISOString(),
293
- ...(exitCode === undefined ? {} : { exitCode }),
475
+ ...(execution?.exitCode !== null && execution?.exitCode !== undefined
476
+ ? { exitCode: execution.exitCode }
477
+ : exitCode === undefined ? {} : { exitCode }),
294
478
  details: {
295
- ...(command === undefined ? {} : { command }),
479
+ ...(effectiveCommand === undefined ? {} : { command: effectiveCommand }),
296
480
  ...(result === undefined ? {} : { result }),
297
481
  ...(details === undefined ? {} : details),
298
482
  verificationCycle: state.verificationCycle ?? 1,
299
483
  ...(classification ? {
300
484
  execution: {
485
+ ...(details?.execution ?? {}),
486
+ ...(execution ? {
487
+ executionRef: execution.executionId,
488
+ argv: [...execution.argv],
489
+ cwd: execution.cwd,
490
+ resolution: execution.resolution,
491
+ status: execution.status,
492
+ exitCode: execution.exitCode,
493
+ } : {}),
301
494
  resolutionMode: classification.resolutionMode,
302
495
  mayInstall: classification.mayInstall,
303
496
  installationAuthorized,
@@ -310,6 +503,7 @@ export async function recordCheck({
310
503
  packageRoot,
311
504
  authorityContext,
312
505
  runtimeContext,
506
+ requireCommandProvenance: observedCommand,
313
507
  });
314
508
  const evidence = createEvidence({
315
509
  kind: evidenceKind,
@@ -349,18 +543,6 @@ export async function recordCheck({
349
543
  authorityContext,
350
544
  runtimeContext,
351
545
  });
352
- const ledger = await validateEventLedger(target, packageRoot);
353
- if (!ledger.valid) {
354
- const first = ledger.errors[0];
355
- throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
356
- }
357
- if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
358
- throw artifactError(
359
- "E_PHASE_CHRONOLOGY_INVALID",
360
- "record-check requires VERIFICATION_STARTED in the current task ledger",
361
- [ARTIFACT_PATHS.events],
362
- );
363
- }
364
546
  const coverage = coverageForRequirements(requiredEvidence, checks, {
365
547
  target,
366
548
  taskId: contract.value.taskId,
@@ -1,5 +1,5 @@
1
1
  import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
- import { requiredEvidenceForTarget } from "./completion-artifacts.js";
2
+ import { requiredEvidenceForTarget, validateChecksExecutionProvenance } from "./completion-artifacts.js";
3
3
  import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
4
4
  import { evaluatePreflight } from "./preflight.js";
5
5
  import { readContract } from "./contract.js";
@@ -42,6 +42,9 @@ function repairNext(error) {
42
42
  case "E_CHECK_INVALID":
43
43
  case "E_CHECK_STATUS_CONTRADICTION":
44
44
  return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
45
+ case "E_COMMAND_PROVENANCE_UNATTESTED":
46
+ case "E_EXECUTION_REF_INVALID":
47
+ return "Run forgeloop run-check with the exact argv, or record the result as manual/NOT_VERIFIED evidence without claiming command execution.";
45
48
  case "E_EVIDENCE_PARTIAL":
46
49
  return "Run or finish the missing component checks and record observed evidence.";
47
50
  case "E_EVIDENCE_INVALID":
@@ -216,6 +219,21 @@ export async function evaluateCompletion({ target, packageRoot, strict = false,
216
219
  }
217
220
  }
218
221
 
222
+ if (contract) {
223
+ errors.push(...await validateChecksExecutionProvenance(state?.checks, {
224
+ target,
225
+ packageRoot,
226
+ taskId: contract.value.taskId,
227
+ artifactPath: ARTIFACT_PATHS.state,
228
+ }));
229
+ errors.push(...await validateChecksExecutionProvenance(receipt?.value?.checks, {
230
+ target,
231
+ packageRoot,
232
+ taskId: contract.value.taskId,
233
+ artifactPath: ARTIFACT_PATHS.receipt,
234
+ }));
235
+ }
236
+
219
237
  if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
220
238
  errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
221
239
  }
@@ -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
+ }