@cassiomc1/forgeloop 1.1.1 → 1.2.2

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 (79) hide show
  1. package/.cursor/rules/project-loop.mdc +1 -1
  2. package/.github/copilot-instructions.md +1 -1
  3. package/AGENTS.md +1 -1
  4. package/CLAUDE.md +1 -1
  5. package/DOCS_INDEX.md +3 -0
  6. package/ENG/design-code-eng.md +124 -0
  7. package/ENG/premium-sites-studio-eng.md +28 -0
  8. package/ENG/taste-frontend-eng.md +3 -2
  9. package/ENG/test-code-eng.md +45 -0
  10. package/LOOP_ENGINEERING.md +74 -0
  11. package/LOOP_SYSTEM_DESIGN.md +9 -5
  12. package/ORCHESTRATOR_INTEGRATION.md +41 -6
  13. package/PROTOCOL_INTEGRATION.md +13 -0
  14. package/README.md +40 -6
  15. package/TERMINOLOGY.md +10 -0
  16. package/THIRD_PARTY_NOTICES.md +58 -1
  17. package/THREAT_MODEL.md +12 -1
  18. package/docs/ARTIFACT_REFERENCE.md +152 -2
  19. package/docs/CLI_REFERENCE.md +346 -30
  20. package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
  21. package/docs/DOCUMENTATION_GUIDE.md +41 -4
  22. package/docs/GETTING_STARTED.md +39 -8
  23. package/docs/RECIPES.md +66 -7
  24. package/docs/TROUBLESHOOTING.md +279 -6
  25. package/package.json +1 -1
  26. package/schemas/policy-baseline.schema.json +26 -0
  27. package/schemas/policy-discovery.schema.json +45 -0
  28. package/schemas/policy-lock.schema.json +16 -0
  29. package/schemas/policy-rules.schema.json +48 -0
  30. package/schemas/policy-snapshot.schema.json +16 -0
  31. package/src/cli.js +102 -1
  32. package/src/commands/baseline.js +120 -0
  33. package/src/commands/init.js +304 -6
  34. package/src/commands/next.js +15 -1
  35. package/src/commands/policy-diff.js +51 -0
  36. package/src/commands/policy-discover.js +42 -0
  37. package/src/commands/policy-status.js +33 -0
  38. package/src/commands/profile-interview.js +50 -0
  39. package/src/commands/progress.js +51 -0
  40. package/src/commands/reconcile-closure.js +49 -0
  41. package/src/commands/record-decision-criterion.js +34 -0
  42. package/src/commands/record-diagnosis.js +49 -0
  43. package/src/commands/rule-verify.js +36 -0
  44. package/src/commands/validate-receipt.js +38 -3
  45. package/src/core/artifact-registry.js +60 -0
  46. package/src/core/audit.js +24 -0
  47. package/src/core/cli-command-definitions.js +163 -7
  48. package/src/core/cli-metadata.js +1 -1
  49. package/src/core/completion-artifacts.js +29 -3
  50. package/src/core/completion.js +101 -10
  51. package/src/core/diagnosis-model.js +214 -0
  52. package/src/core/diagnosis.js +171 -0
  53. package/src/core/error-codes.js +292 -0
  54. package/src/core/events.js +47 -1
  55. package/src/core/execution-prerequisites.js +38 -20
  56. package/src/core/execution.js +20 -3
  57. package/src/core/native-adapters.js +14 -4
  58. package/src/core/next-action-model.js +40 -5
  59. package/src/core/next-action.js +234 -91
  60. package/src/core/phase.js +29 -0
  61. package/src/core/policy-adapters.js +276 -0
  62. package/src/core/policy-baseline.js +144 -0
  63. package/src/core/policy-diff.js +133 -0
  64. package/src/core/policy-discovery.js +225 -0
  65. package/src/core/policy-engine.js +533 -0
  66. package/src/core/policy-mutation.js +139 -0
  67. package/src/core/preflight-consistency.js +23 -15
  68. package/src/core/preflight-model.js +10 -2
  69. package/src/core/preflight.js +65 -1
  70. package/src/core/progress.js +143 -0
  71. package/src/core/protocol.js +8 -0
  72. package/src/core/reconcile-closure.js +173 -0
  73. package/src/core/schema-validation.js +6 -0
  74. package/src/core/settlement-model.js +85 -0
  75. package/src/core/settlement.js +78 -0
  76. package/src/core/task-context.js +11 -0
  77. package/src/core/task-discovery.js +67 -1
  78. package/src/core/task-paths.js +9 -0
  79. package/src/core/templates.js +5 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Canonical, declarative definition of all 33 ForgeLoop CLI commands.
2
+ * Canonical, declarative definition of all 42 ForgeLoop CLI commands.
3
3
  * This is the machine source of truth for CLI option parsing, help text,
4
4
  * metadata, documentation generation, and conformance validation.
5
5
  *
@@ -64,7 +64,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
64
64
  mutation: "MUTATING",
65
65
  options: Object.freeze({
66
66
  ...CLI_COMMON_OPTIONS,
67
- "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "show planned writes without changing files" }),
67
+ "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "perform deterministic init planning and conflict detection without writing" }),
68
68
  }),
69
69
  writes: [".forgeloop/*", "AGENTS.md", "CLAUDE.md", ".cursor/rules/project-loop.mdc", ".github/copilot-instructions.md"],
70
70
  removes: [],
@@ -93,7 +93,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
93
93
  mutation: "MUTATING",
94
94
  options: Object.freeze({
95
95
  ...CLI_COMMON_OPTIONS,
96
- "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "show planned writes without changing files" }),
96
+ "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "perform deterministic update planning and conflict detection without writing" }),
97
97
  }),
98
98
  writes: [".forgeloop/*", "AGENTS.md", "CLAUDE.md", ".cursor/rules/project-loop.mdc", ".github/copilot-instructions.md"],
99
99
  removes: [],
@@ -317,6 +317,55 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
317
317
  mayExecuteExternalProcess: false,
318
318
  description: "Records external terminal result evidence (PUBLICATION or PRODUCTION_READINESS) into receipt.",
319
319
  }),
320
+ "record-diagnosis": Object.freeze({
321
+ name: "record-diagnosis",
322
+ category: "lifecycle",
323
+ mutation: "MUTATING",
324
+ options: Object.freeze({
325
+ ...CLI_COMMON_OPTIONS,
326
+ ...CLI_TASK_OPTION,
327
+ "--hypothesis": Object.freeze({ targetKey: "hypothesis", parseType: "string", takesValue: true, valueName: "text", missingValueMessage: "--hypothesis requires text", description: "specific root-cause hypothesis explaining the verification failure" }),
328
+ "--failure-class": Object.freeze({ targetKey: "failureClass", parseType: "string", takesValue: true, valueName: "class", missingValueMessage: "--failure-class requires a class", description: "canonical failure class taxonomy" }),
329
+ "--evidence-ref": Object.freeze({ targetKey: "evidenceRefs", parseType: "string", takesValue: true, valueName: "check-id", repeatable: true, missingValueMessage: "--evidence-ref requires a check ID", description: "reference to failed/blocked check from current cycle" }),
330
+ "--settled-by": Object.freeze({ targetKey: "settledBy", parseType: "string", takesValue: true, valueName: "text", missingValueMessage: "--settled-by requires text", description: "falsification or settlement criteria for the hypothesis" }),
331
+ "--next-safe-action": Object.freeze({ targetKey: "nextSafeAction", parseType: "string", takesValue: true, valueName: "text", missingValueMessage: "--next-safe-action requires text", description: "smallest safe action to address the hypothesis" }),
332
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
333
+ }),
334
+ writes: [".forgeloop/task-state/<taskKey>/events.ndjson", ".forgeloop/task-state/<taskKey>/work-state.json"],
335
+ removes: [],
336
+ mayExecuteExternalProcess: false,
337
+ description: "Records an append-only diagnosis event in the lifecycle event ledger for the active cycle.",
338
+ }),
339
+ progress: Object.freeze({
340
+ name: "progress",
341
+ category: "diagnostics",
342
+ mutation: "READ_ONLY",
343
+ options: Object.freeze({
344
+ ...CLI_COMMON_OPTIONS,
345
+ ...CLI_TASK_OPTION,
346
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit progress evaluation as JSON" }),
347
+ }),
348
+ writes: [],
349
+ removes: [],
350
+ mayExecuteExternalProcess: false,
351
+ description: "Evaluates task progress across verification cycles and detects stalls deterministically.",
352
+ }),
353
+ "record-decision-criterion": Object.freeze({
354
+ name: "record-decision-criterion",
355
+ category: "lifecycle",
356
+ mutation: "MUTATING",
357
+ options: Object.freeze({
358
+ ...CLI_COMMON_OPTIONS,
359
+ ...CLI_TASK_OPTION,
360
+ "--decision": Object.freeze({ targetKey: "decision", parseType: "string", takesValue: true, valueName: "text", missingValueMessage: "--decision requires text", description: "unresolved decision text matching current contract" }),
361
+ "--settled-by": Object.freeze({ targetKey: "settledBy", parseType: "string", takesValue: true, valueName: "text", missingValueMessage: "--settled-by requires text", description: "criteria or guidance that settles the decision" }),
362
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
363
+ }),
364
+ writes: [".forgeloop/task-state/<taskKey>/events.ndjson"],
365
+ removes: [],
366
+ mayExecuteExternalProcess: false,
367
+ description: "Records an append-only decision settlement criterion bound to the active contract fingerprint.",
368
+ }),
320
369
  complete: Object.freeze({
321
370
  name: "complete",
322
371
  category: "lifecycle",
@@ -377,18 +426,106 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
377
426
  mayExecuteExternalProcess: false,
378
427
  description: "Evaluates active task state against named enterprise policy packs.",
379
428
  }),
380
- bundle: Object.freeze({
381
- name: "bundle",
429
+ "policy-discover": Object.freeze({
430
+ name: "policy-discover",
431
+ category: "policy-audit",
432
+ mutation: "MUTATING",
433
+ options: Object.freeze({
434
+ ...CLI_COMMON_OPTIONS,
435
+ "--write": Object.freeze({ targetKey: "write", parseType: "boolean", takesValue: false, description: "persist discovered policy to .forgeloop/policy/discovery.json" }),
436
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
437
+ }),
438
+ writes: [".forgeloop/policy/discovery.json", ".forgeloop/policy/policy.lock"],
439
+ removes: [],
440
+ mayExecuteExternalProcess: false,
441
+ description: "Discovers repository policy facts and candidate rules deterministically.",
442
+ }),
443
+ "policy-status": Object.freeze({
444
+ name: "policy-status",
445
+ category: "policy-audit",
446
+ mutation: "READ_ONLY",
447
+ options: Object.freeze({
448
+ ...CLI_COMMON_OPTIONS,
449
+ ...CLI_TASK_OPTION,
450
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
451
+ }),
452
+ writes: [],
453
+ removes: [],
454
+ mayExecuteExternalProcess: false,
455
+ description: "Evaluates repository and task state against effective policy rules and baseline.",
456
+ }),
457
+ "policy-diff": Object.freeze({
458
+ name: "policy-diff",
382
459
  category: "policy-audit",
383
460
  mutation: "READ_ONLY",
384
461
  options: Object.freeze({
385
462
  ...CLI_COMMON_OPTIONS,
386
463
  ...CLI_TASK_OPTION,
464
+ "--before": Object.freeze({ targetKey: "before", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--before requires a path", description: "path to before policy JSON" }),
465
+ "--after": Object.freeze({ targetKey: "after", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--after requires a path", description: "path to after policy JSON" }),
387
466
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
388
467
  }),
389
468
  writes: [],
390
469
  removes: [],
391
470
  mayExecuteExternalProcess: false,
471
+ description: "Performs semantic diffing between policy versions to detect tightening or weakening.",
472
+ }),
473
+ "rule-verify": Object.freeze({
474
+ name: "rule-verify",
475
+ category: "policy-audit",
476
+ mutation: "READ_ONLY",
477
+ options: Object.freeze({
478
+ ...CLI_COMMON_OPTIONS,
479
+ "--rule": Object.freeze({ targetKey: "rule", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--rule requires an ID", description: "verify a specific policy rule ID" }),
480
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
481
+ }),
482
+ writes: [],
483
+ removes: [],
484
+ mayExecuteExternalProcess: false,
485
+ description: "Verifies policy rules against mutation fixtures to prove detector efficacy.",
486
+ }),
487
+ baseline: Object.freeze({
488
+ name: "baseline",
489
+ category: "policy-audit",
490
+ mutation: "MUTATING",
491
+ options: Object.freeze({
492
+ ...CLI_COMMON_OPTIONS,
493
+ "--record": Object.freeze({ targetKey: "record", parseType: "boolean", takesValue: false, description: "record current violations as brownfield baseline" }),
494
+ "--update": Object.freeze({ targetKey: "update", parseType: "boolean", takesValue: false, description: "ratchet baseline downward by removing resolved violations" }),
495
+ "--policy-reset-authorized": Object.freeze({ targetKey: "policyResetAuthorized", parseType: "boolean", takesValue: false, description: "explicit operator authority to re-record baseline during active tasks" }),
496
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
497
+ }),
498
+ writes: [".forgeloop/policy/baseline.json", ".forgeloop/policy/policy.lock"],
499
+ removes: [],
500
+ mayExecuteExternalProcess: false,
501
+ description: "Manages brownfield policy baseline violations with monotonic downward ratcheting.",
502
+ }),
503
+ "profile-interview": Object.freeze({
504
+ name: "profile-interview",
505
+ category: "diagnostics",
506
+ mutation: "READ_ONLY",
507
+ options: Object.freeze({
508
+ ...CLI_COMMON_OPTIONS,
509
+ "--dry-run": Object.freeze({ targetKey: "dryRun", parseType: "boolean", takesValue: false, description: "show planned interview questions without changing files" }),
510
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
511
+ }),
512
+ writes: [],
513
+ removes: [],
514
+ mayExecuteExternalProcess: false,
515
+ description: "Optional interactive or dry-run interview to refine project profile facts.",
516
+ }),
517
+ bundle: Object.freeze({
518
+ name: "bundle",
519
+ category: "policy-audit",
520
+ mutation: "MUTATING",
521
+ options: Object.freeze({
522
+ ...CLI_COMMON_OPTIONS,
523
+ ...CLI_TASK_OPTION,
524
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
525
+ }),
526
+ writes: [".forgeloop/tasks/<taskId>"],
527
+ removes: [],
528
+ mayExecuteExternalProcess: false,
392
529
  description: "Exports current task artifacts into a portable task bundle archive.",
393
530
  }),
394
531
  inspect: Object.freeze({
@@ -449,13 +586,32 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
449
586
  mayExecuteExternalProcess: false,
450
587
  description: "Removes work-state.json for the active task only, preserving sibling contract, routing, and ledger files.",
451
588
  }),
589
+ "reconcile-closure": Object.freeze({
590
+ name: "reconcile-closure",
591
+ category: "lifecycle",
592
+ mutation: "MUTATING",
593
+ options: Object.freeze({
594
+ ...CLI_COMMON_OPTIONS,
595
+ ...CLI_TASK_OPTION,
596
+ "--id": Object.freeze({ targetKey: "checkId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--id requires a check ID", description: "contract verification item id used as evidence" }),
597
+ "--requirement": Object.freeze({ targetKey: "checkRequirement", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--requirement requires an evidence target", description: "exact contract verification item requirement text" }),
598
+ "--details": Object.freeze({ targetKey: "checkDetails", parseType: "json-object", takesValue: true, valueName: "json", missingValueMessage: "--details requires a JSON object", description: "additional structured execution details" }),
599
+ "--": Object.freeze({ targetKey: "commandArgv", parseType: "argv", takesValue: true, valueName: "argv...", description: "exact command argv to execute as objective-satisfaction evidence" }),
600
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
601
+ }),
602
+ writes: [".forgeloop/task-state/<taskKey>/executions/exec-<id>.json", ".forgeloop/task-state/<taskKey>/work-state.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
603
+ removes: [],
604
+ mayExecuteExternalProcess: true,
605
+ description: "Refreshes the work-state checkpoint of an EXECUTING task whose objective is already satisfied in the current repository, after contract-bound executed evidence, so canonical completion can proceed.",
606
+ }),
452
607
  "validate-receipt": Object.freeze({
453
608
  name: "validate-receipt",
454
609
  category: "verification",
455
610
  mutation: "READ_ONLY",
456
611
  options: Object.freeze({
457
612
  ...CLI_COMMON_OPTIONS,
458
- "--file": Object.freeze({ targetKey: "file", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--file requires a path", description: "receipt file relative to target" }),
613
+ ...CLI_TASK_OPTION,
614
+ "--file": Object.freeze({ targetKey: "file", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--file requires a path", description: "receipt file relative to target (overrides task-based receipt resolution)" }),
459
615
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
460
616
  }),
461
617
  writes: [],
@@ -495,7 +651,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
495
651
  "--contract-file": Object.freeze({ targetKey: "contractFile", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--contract-file requires a path", description: "path to initial contract file" }),
496
652
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
497
653
  }),
498
- writes: [".forgeloop/task-state/<taskKey>/task.json", ".forgeloop/task-state/<taskKey>/current-contract.json"],
654
+ writes: [".forgeloop/task-state/<taskKey>/task.json", ".forgeloop/task-state/<taskKey>/contract.json"],
499
655
  removes: [],
500
656
  mayExecuteExternalProcess: false,
501
657
  description: "Creates a new task descriptor and initializes its isolated task state namespace.",
@@ -1,7 +1,7 @@
1
1
  import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
2
2
 
3
3
  /**
4
- * Canonical metadata for all 27 ForgeLoop CLI commands derived directly
4
+ * Canonical metadata for all 43 ForgeLoop CLI commands derived directly
5
5
  * from CLI_COMMAND_DEFINITIONS to guarantee zero divergence.
6
6
  */
7
7
  export const CLI_COMMAND_METADATA = Object.freeze(
@@ -26,6 +26,32 @@ import { readTaskDescriptor } from "./task-descriptor.js";
26
26
  import { assertClaimsCoverChangedPaths } from "./task-scope.js";
27
27
  import { discoverTasks } from "./task-discovery.js";
28
28
 
29
+ /**
30
+ * Canonical terminal-result types shared by runtime validation, tests, and
31
+ * documentation conformance. Keep these single-sourced; do not re-declare
32
+ * the same type lists in validators or tests.
33
+ */
34
+ export const TERMINAL_RESULT_TYPES = Object.freeze([
35
+ "PUBLICATION",
36
+ "PRODUCTION_READINESS",
37
+ ]);
38
+
39
+ /**
40
+ * Canonical terminal-status sets shared by runtime validation, tests, and
41
+ * documentation conformance. Keep these single-sourced; do not re-declare
42
+ * the same status lists in validators or tests.
43
+ */
44
+ export const PUBLICATION_STATUSES = Object.freeze([
45
+ "committed",
46
+ "pushed",
47
+ "published",
48
+ "deployed",
49
+ ]);
50
+ export const PRODUCTION_READINESS_STATUSES = Object.freeze([
51
+ "ready",
52
+ "blocked",
53
+ ]);
54
+
29
55
  function artifactError(code, message, artifacts = []) {
30
56
  const error = new Error(message);
31
57
  error.code = code;
@@ -706,13 +732,13 @@ export async function recordTerminalResult({
706
732
  if (!target || !requirement || !type || !status || !source || !result) {
707
733
  throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
708
734
  }
709
- if (!["PUBLICATION", "PRODUCTION_READINESS"].includes(type)) {
735
+ if (!TERMINAL_RESULT_TYPES.includes(type)) {
710
736
  throw artifactError("E_FUTURE_TERMINAL_EVIDENCE", `record-terminal-result does not support type ${type}`, [ARTIFACT_PATHS.state]);
711
737
  }
712
- if (type === "PUBLICATION" && !["committed", "pushed", "published", "deployed"].includes(status)) {
738
+ if (type === "PUBLICATION" && !PUBLICATION_STATUSES.includes(status)) {
713
739
  throw artifactError("E_CHECK_INVALID", `Invalid publication status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
714
740
  }
715
- if (type === "PRODUCTION_READINESS" && !["ready", "blocked"].includes(status)) {
741
+ if (type === "PRODUCTION_READINESS" && !PRODUCTION_READINESS_STATUSES.includes(status)) {
716
742
  throw artifactError("E_CHECK_INVALID", `Invalid production readiness status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
717
743
  }
718
744
 
@@ -11,7 +11,29 @@ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
11
11
  import { evaluateStartExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
12
12
  import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
13
13
  import { evaluateTerminalRequirements } from "./evidence-readiness.js";
14
- import { taskArtifactPath } from "./task-paths.js";
14
+ import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
15
+ import { detectPolicyCapability, evaluateTargetPolicy } from "./policy-engine.js";
16
+
17
+ /**
18
+ * Canonical completion return statuses shared by the runtime, tests, and
19
+ * documentation conformance. The CLI reference's return-status prose is
20
+ * mechanically checked against this set.
21
+ */
22
+ export const COMPLETION_STATUSES = Object.freeze(["VALID", "REJECTED"]);
23
+
24
+ /**
25
+ * Canonical completion verification-status values returned by
26
+ * evaluateCompletion. The asymmetric casing (VALID / invalid) is the actual
27
+ * runtime contract and is intentionally preserved; documentation conformance
28
+ * checks documented examples and prose against this exact set, and the runtime
29
+ * derives its output from these same named constants.
30
+ */
31
+ export const VERIFICATION_STATUS_VALID = "VALID";
32
+ export const VERIFICATION_STATUS_INVALID = "invalid";
33
+ export const VERIFICATION_STATUSES = Object.freeze([
34
+ VERIFICATION_STATUS_VALID,
35
+ VERIFICATION_STATUS_INVALID,
36
+ ]);
15
37
 
16
38
  function issue(code, message, artifacts = [], details = {}) {
17
39
  return { code, message, artifacts, ...details };
@@ -74,13 +96,20 @@ function repairNext(error) {
74
96
  return "Do not edit work-state or receipt manually; recover through supported lifecycle commands.";
75
97
  case "E_COMPLETION_RECOVERY_UNAUTHORIZED":
76
98
  case "E_COMPLETION_REJECTION_LEDGER_MISMATCH":
77
- case "E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH":
78
- case "E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH":
79
- return "Ensure a matching completion rejection exists in the protocol ledger and artifacts remain unmodified before recovery.";
80
- case "E_GATE_UNVERIFIED":
81
- case "E_GATE_STALE":
82
- return "Satisfy or refresh the named gate, then rerun forgeloop preflight.";
83
- case "E_PROFILE_UNVERIFIED":
99
+ case "E_CYCLE_CLOSED":
100
+ return "Advance the task through valid lifecycle phases (PLANNED -> EXECUTING -> VERIFYING -> REVIEWING).";
101
+ case "E_LEDGER_INVALID":
102
+ case "E_LEDGER_STALE":
103
+ case "E_STATE_LEDGER_MISMATCH":
104
+ return "Inspect the event ledger and repair sequence or integrity violations.";
105
+ case "E_EVIDENCE_MISSING":
106
+ case "E_EVIDENCE_BLOCKED":
107
+ case "E_CHECK_FAILED":
108
+ case "E_REQUIREMENT_UNMET":
109
+ return "Execute and pass all required checks using forgeloop run-check before completion.";
110
+ case "E_CHECK_EXECUTION_PROVENANCE_MISSING":
111
+ return "Re-run checks via forgeloop run-check to ensure ForgeLoop execution provenance.";
112
+ case "E_CONTRACT_PROFILE_STRICT_UNVERIFIED":
84
113
  return "Use Standard mode for a fresh target, or verify PROJECT_PROFILE.md before Strict completion.";
85
114
  case "E_INSTALLATION_AUTHORITY_REQUIRED":
86
115
  case "E_AUTHORITY_INVALID":
@@ -89,6 +118,28 @@ function repairNext(error) {
89
118
  return "Do not execute installation-capable verification commands without explicit scoped installation authority; use local equivalents or record NOT_VERIFIED.";
90
119
  case "E_VERIFICATION_TOOL_UNAVAILABLE":
91
120
  return "Use an available local verifier, an existing equivalent, or record NOT_VERIFIED if installation was not authorized.";
121
+ case "E_NEW_POLICY_VIOLATION":
122
+ return "Resolve the new policy violation or record baseline if adopted debt before completion.";
123
+ case "E_POLICY_WEAKENING":
124
+ return "Restore the original policy configuration or obtain explicit project authority before retrying completion.";
125
+ case "E_CHECK_INERT":
126
+ return "Configure an applicable target scope or mark the inert check unsupported.";
127
+ case "E_CHECK_MUTATION_NOT_DETECTED":
128
+ return "Fix checker logic to properly detect intentional mutation fixtures.";
129
+ case "E_CHECK_MUTATION_EXECUTION_ERROR":
130
+ return "Repair the checker execution path and rerun rule verification.";
131
+ case "E_POLICY_LOCK_MISMATCH":
132
+ return "Re-evaluate effective rules and update policy.lock or restore modified rules.";
133
+ case "E_POLICY_DRIFT":
134
+ case "E_POLICY_DRIFT_UNKNOWN":
135
+ return "Re-verify affected checks or restore original policy.";
136
+ case "E_POLICY_INVALID":
137
+ return "Validate and repair rules.json, baseline.json, or discovery.json against schema.";
138
+ case "E_POLICY_EVALUATION_FAILED":
139
+ return "Inspect policy configuration and checker adapters for unhandled errors.";
140
+ case "E_BASELINE_EXPANSION":
141
+ case "E_BASELINE_RECORD_DURING_ACTIVE_TASK":
142
+ return "Resolve new violations rather than expanding the baseline.";
92
143
  default:
93
144
  return "Resolve this validator finding in the named artifact before retrying completion.";
94
145
  }
@@ -333,12 +384,52 @@ export async function evaluateCompletion({
333
384
  }
334
385
  }
335
386
 
387
+ const policyCapability = await detectPolicyCapability(target, packageRoot);
388
+ if (policyCapability === "INVALID") {
389
+ errors.push(issue(
390
+ "E_POLICY_INVALID",
391
+ "Policy configuration or baseline artifacts are malformed or fail schema validation.",
392
+ [PROJECT_ARTIFACT_PATHS.policyRules, PROJECT_ARTIFACT_PATHS.policyBaseline],
393
+ ));
394
+ } else if (policyCapability === "AVAILABLE") {
395
+ try {
396
+ const policyEval = await evaluateTargetPolicy({
397
+ target,
398
+ packageRoot,
399
+ taskId: contract?.value?.taskId ?? taskId,
400
+ });
401
+ for (const policyErr of policyEval.errors ?? []) {
402
+ const code = policyErr.code === "NEW_VIOLATION" ? "E_NEW_POLICY_VIOLATION"
403
+ : policyErr.code === "POLICY_WEAKENING" ? "E_POLICY_WEAKENING"
404
+ : policyErr.code === "CHECK_INERT" ? "E_CHECK_INERT"
405
+ : policyErr.code === "CHECK_MUTATION_NOT_DETECTED" ? "E_CHECK_MUTATION_NOT_DETECTED"
406
+ : policyErr.code === "CHECK_MUTATION_EXECUTION_ERROR" ? "E_CHECK_MUTATION_EXECUTION_ERROR"
407
+ : policyErr.code === "POLICY_LOCK_MISMATCH" ? "E_POLICY_LOCK_MISMATCH"
408
+ : policyErr.code === "POLICY_DRIFT_UNKNOWN" ? "E_POLICY_DRIFT_UNKNOWN"
409
+ : policyErr.code === "POLICY_EVALUATION_FAILED" ? "E_POLICY_EVALUATION_FAILED"
410
+ : policyErr.code;
411
+ errors.push(issue(
412
+ code,
413
+ policyErr.why || policyErr.message,
414
+ [PROJECT_ARTIFACT_PATHS.policyLock],
415
+ { ruleId: policyErr.ruleId, fix: policyErr.fix },
416
+ ));
417
+ }
418
+ } catch (error) {
419
+ errors.push(issue(
420
+ "E_POLICY_EVALUATION_FAILED",
421
+ `Policy evaluation threw an unexpected error: ${error.message}`,
422
+ [PROJECT_ARTIFACT_PATHS.policyLock],
423
+ ));
424
+ }
425
+ }
426
+
336
427
  const sortedErrors = sortIssues(errors);
337
428
  const valid = sortedErrors.length === 0;
338
429
  return {
339
- status: valid ? "VALID" : "REJECTED",
430
+ status: valid ? COMPLETION_STATUSES[0] : COMPLETION_STATUSES[1],
340
431
  taskStatus: valid ? "COMPLETE" : receiptValue?.status === "blocked" ? "BLOCKED" : "INCOMPLETE",
341
- verificationStatus: valid ? "VALID" : "invalid",
432
+ verificationStatus: valid ? VERIFICATION_STATUS_VALID : VERIFICATION_STATUS_INVALID,
342
433
  publicationStatus: publication,
343
434
  productionReadiness: receiptValue?.productionReadiness ?? "not-verified",
344
435
  errors: sortedErrors,
@@ -0,0 +1,214 @@
1
+ import { canonicalFingerprint } from "./artifacts.js";
2
+ import { assertFailureClass } from "./protocol.js";
3
+
4
+ export const DIAGNOSIS_INFORMATION_GAIN = Object.freeze([
5
+ "FIRST_DIAGNOSIS",
6
+ "NEW_HYPOTHESIS",
7
+ "NEW_EVIDENCE",
8
+ "NEW_HYPOTHESIS_AND_EVIDENCE",
9
+ "NONE",
10
+ ]);
11
+
12
+ export function normalizeDiagnosisText(value, label = "value") {
13
+ if (typeof value !== "string" || !value.trim()) {
14
+ const error = new Error(`${label} must be a non-empty string`);
15
+ error.code = "E_DIAGNOSIS_INVALID";
16
+ throw error;
17
+ }
18
+ return value.trim().replace(/\s+/gu, " ").toLowerCase();
19
+ }
20
+
21
+ export function diagnosisFingerprint({ failureClass, hypothesis, evidenceRefs }) {
22
+ assertFailureClass(failureClass);
23
+ const normalizedHypothesis = normalizeDiagnosisText(hypothesis, "hypothesis");
24
+ if (!Array.isArray(evidenceRefs) || evidenceRefs.length === 0) {
25
+ const error = new Error("evidenceRefs must be a non-empty array of strings");
26
+ error.code = "E_DIAGNOSIS_INVALID";
27
+ throw error;
28
+ }
29
+ const cleanRefs = evidenceRefs.map((ref) => {
30
+ if (typeof ref !== "string" || !ref.trim()) {
31
+ const error = new Error("evidenceRef must be a non-empty string");
32
+ error.code = "E_DIAGNOSIS_INVALID";
33
+ throw error;
34
+ }
35
+ return ref.trim();
36
+ });
37
+ const normalizedRefs = [...new Set(cleanRefs)].sort();
38
+ return canonicalFingerprint({
39
+ failureClass,
40
+ hypothesis: normalizedHypothesis,
41
+ evidenceRefs: normalizedRefs,
42
+ });
43
+ }
44
+
45
+ export function classifyDiagnosisInformationGain(current, previous) {
46
+ if (!previous) return "FIRST_DIAGNOSIS";
47
+
48
+ const currentHypothesis = normalizeDiagnosisText(current.hypothesis, "current hypothesis");
49
+ const prevHypothesis = normalizeDiagnosisText(previous.hypothesis, "previous hypothesis");
50
+ const sameHypothesis = currentHypothesis === prevHypothesis;
51
+
52
+ const currentEvidence = [...new Set((current.evidenceRefs ?? []).map((r) => String(r).trim()))].sort();
53
+ const prevEvidence = [...new Set((previous.evidenceRefs ?? []).map((r) => String(r).trim()))].sort();
54
+ const sameEvidence = JSON.stringify(currentEvidence) === JSON.stringify(prevEvidence);
55
+
56
+ if (sameHypothesis && sameEvidence) return "NONE";
57
+ if (!sameHypothesis && !sameEvidence) return "NEW_HYPOTHESIS_AND_EVIDENCE";
58
+ if (!sameHypothesis) return "NEW_HYPOTHESIS";
59
+ return "NEW_EVIDENCE";
60
+ }
61
+
62
+ export function createDiagnosisDetails(input, previous = null) {
63
+ if (typeof input?.verificationCycle !== "number" || !Number.isInteger(input.verificationCycle) || input.verificationCycle < 1) {
64
+ const error = new Error("verificationCycle must be an integer >= 1");
65
+ error.code = "E_DIAGNOSIS_INVALID";
66
+ throw error;
67
+ }
68
+ try {
69
+ assertFailureClass(input.failureClass);
70
+ } catch {
71
+ const error = new Error(`Invalid failureClass: ${input.failureClass}`);
72
+ error.code = "E_DIAGNOSIS_INVALID";
73
+ throw error;
74
+ }
75
+ if (typeof input.hypothesis !== "string" || !input.hypothesis.trim()) {
76
+ const error = new Error("hypothesis must be a non-empty string");
77
+ error.code = "E_DIAGNOSIS_INVALID";
78
+ throw error;
79
+ }
80
+ if (!Array.isArray(input.evidenceRefs) || input.evidenceRefs.length === 0) {
81
+ const error = new Error("evidenceRefs must be a non-empty array");
82
+ error.code = "E_DIAGNOSIS_INVALID";
83
+ throw error;
84
+ }
85
+ const cleanRefs = input.evidenceRefs.map((r) => {
86
+ if (typeof r !== "string" || !r.trim()) {
87
+ const error = new Error("each evidenceRef must be a non-empty string");
88
+ error.code = "E_DIAGNOSIS_INVALID";
89
+ throw error;
90
+ }
91
+ return r.trim();
92
+ });
93
+ const uniqueRefs = [...new Set(cleanRefs)].sort();
94
+ if (typeof input.settledBy !== "string" || !input.settledBy.trim()) {
95
+ const error = new Error("settledBy must be a non-empty string");
96
+ error.code = "E_DIAGNOSIS_INVALID";
97
+ throw error;
98
+ }
99
+ if (typeof input.nextSafeAction !== "string" || !input.nextSafeAction.trim()) {
100
+ const error = new Error("nextSafeAction must be a non-empty string");
101
+ error.code = "E_DIAGNOSIS_INVALID";
102
+ throw error;
103
+ }
104
+
105
+ const fingerprint = diagnosisFingerprint({
106
+ failureClass: input.failureClass,
107
+ hypothesis: input.hypothesis,
108
+ evidenceRefs: uniqueRefs,
109
+ });
110
+ const infoGain = classifyDiagnosisInformationGain(
111
+ { hypothesis: input.hypothesis, evidenceRefs: uniqueRefs },
112
+ previous ? { hypothesis: previous.hypothesis, evidenceRefs: previous.evidenceRefs } : null,
113
+ );
114
+
115
+ return {
116
+ verificationCycle: input.verificationCycle,
117
+ failureClass: input.failureClass,
118
+ hypothesis: input.hypothesis.trim(),
119
+ evidenceRefs: uniqueRefs,
120
+ settledBy: input.settledBy.trim(),
121
+ nextSafeAction: input.nextSafeAction.trim(),
122
+ diagnosisFingerprint: fingerprint,
123
+ informationGain: infoGain,
124
+ previousDiagnosisFingerprint: previous?.diagnosisFingerprint ?? null,
125
+ };
126
+ }
127
+
128
+ export function assertDiagnosisDetails(details) {
129
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
130
+ const error = new Error("Diagnosis details must be an object");
131
+ error.code = "E_DIAGNOSIS_INVALID";
132
+ throw error;
133
+ }
134
+ if (typeof details.verificationCycle !== "number" || !Number.isInteger(details.verificationCycle) || details.verificationCycle < 1) {
135
+ const error = new Error("Diagnosis verificationCycle must be an integer >= 1");
136
+ error.code = "E_DIAGNOSIS_INVALID";
137
+ throw error;
138
+ }
139
+ try {
140
+ assertFailureClass(details.failureClass);
141
+ } catch {
142
+ const error = new Error(`Invalid diagnosis failureClass: ${details.failureClass}`);
143
+ error.code = "E_DIAGNOSIS_INVALID";
144
+ throw error;
145
+ }
146
+ if (typeof details.hypothesis !== "string" || !details.hypothesis.trim()) {
147
+ const error = new Error("Diagnosis hypothesis must be a non-empty string");
148
+ error.code = "E_DIAGNOSIS_INVALID";
149
+ throw error;
150
+ }
151
+ if (!Array.isArray(details.evidenceRefs) || details.evidenceRefs.length === 0) {
152
+ const error = new Error("Diagnosis evidenceRefs must be a non-empty array");
153
+ error.code = "E_DIAGNOSIS_INVALID";
154
+ throw error;
155
+ }
156
+ for (const ref of details.evidenceRefs) {
157
+ if (typeof ref !== "string" || !ref.trim()) {
158
+ const error = new Error("Diagnosis evidenceRef must be a non-empty string");
159
+ error.code = "E_DIAGNOSIS_INVALID";
160
+ throw error;
161
+ }
162
+ }
163
+ if (typeof details.settledBy !== "string" || !details.settledBy.trim()) {
164
+ const error = new Error("Diagnosis settledBy must be a non-empty string");
165
+ error.code = "E_DIAGNOSIS_INVALID";
166
+ throw error;
167
+ }
168
+ if (typeof details.nextSafeAction !== "string" || !details.nextSafeAction.trim()) {
169
+ const error = new Error("Diagnosis nextSafeAction must be a non-empty string");
170
+ error.code = "E_DIAGNOSIS_INVALID";
171
+ throw error;
172
+ }
173
+ if (!DIAGNOSIS_INFORMATION_GAIN.includes(details.informationGain)) {
174
+ const error = new Error(`Invalid diagnosis informationGain: ${details.informationGain}`);
175
+ error.code = "E_DIAGNOSIS_INVALID";
176
+ throw error;
177
+ }
178
+ if (typeof details.diagnosisFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(details.diagnosisFingerprint)) {
179
+ const error = new Error("Invalid diagnosisFingerprint");
180
+ error.code = "E_DIAGNOSIS_INVALID";
181
+ throw error;
182
+ }
183
+ const computed = diagnosisFingerprint({
184
+ failureClass: details.failureClass,
185
+ hypothesis: details.hypothesis,
186
+ evidenceRefs: details.evidenceRefs,
187
+ });
188
+ if (computed !== details.diagnosisFingerprint) {
189
+ const error = new Error("Diagnosis fingerprint does not match computed fingerprint");
190
+ error.code = "E_DIAGNOSIS_INVALID";
191
+ throw error;
192
+ }
193
+ if (details.previousDiagnosisFingerprint !== null && (typeof details.previousDiagnosisFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(details.previousDiagnosisFingerprint))) {
194
+ const error = new Error("Invalid previousDiagnosisFingerprint");
195
+ error.code = "E_DIAGNOSIS_INVALID";
196
+ throw error;
197
+ }
198
+ return details;
199
+ }
200
+
201
+ export function diagnosisEventsForTask(events, taskId) {
202
+ if (!Array.isArray(events)) return [];
203
+ return events.filter((e) => e.event === "DIAGNOSIS_RECORDED" && (!taskId || e.taskId === taskId));
204
+ }
205
+
206
+ export function currentCycleDiagnosis(events, taskId, verificationCycle) {
207
+ const taskEvents = diagnosisEventsForTask(events, taskId);
208
+ for (let i = taskEvents.length - 1; i >= 0; i--) {
209
+ if (taskEvents[i].details?.verificationCycle === verificationCycle) {
210
+ return taskEvents[i];
211
+ }
212
+ }
213
+ return null;
214
+ }