@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.
- package/.cursor/rules/project-loop.mdc +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/DOCS_INDEX.md +3 -0
- package/ENG/design-code-eng.md +124 -0
- package/ENG/premium-sites-studio-eng.md +28 -0
- package/ENG/taste-frontend-eng.md +3 -2
- package/ENG/test-code-eng.md +45 -0
- package/LOOP_ENGINEERING.md +74 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +41 -6
- package/PROTOCOL_INTEGRATION.md +13 -0
- package/README.md +40 -6
- package/TERMINOLOGY.md +10 -0
- package/THIRD_PARTY_NOTICES.md +58 -1
- package/THREAT_MODEL.md +12 -1
- package/docs/ARTIFACT_REFERENCE.md +152 -2
- package/docs/CLI_REFERENCE.md +346 -30
- package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
- package/docs/DOCUMENTATION_GUIDE.md +41 -4
- package/docs/GETTING_STARTED.md +39 -8
- package/docs/RECIPES.md +66 -7
- package/docs/TROUBLESHOOTING.md +279 -6
- package/package.json +1 -1
- package/schemas/policy-baseline.schema.json +26 -0
- package/schemas/policy-discovery.schema.json +45 -0
- package/schemas/policy-lock.schema.json +16 -0
- package/schemas/policy-rules.schema.json +48 -0
- package/schemas/policy-snapshot.schema.json +16 -0
- package/src/cli.js +102 -1
- package/src/commands/baseline.js +120 -0
- package/src/commands/init.js +304 -6
- package/src/commands/next.js +15 -1
- package/src/commands/policy-diff.js +51 -0
- package/src/commands/policy-discover.js +42 -0
- package/src/commands/policy-status.js +33 -0
- package/src/commands/profile-interview.js +50 -0
- package/src/commands/progress.js +51 -0
- package/src/commands/reconcile-closure.js +49 -0
- package/src/commands/record-decision-criterion.js +34 -0
- package/src/commands/record-diagnosis.js +49 -0
- package/src/commands/rule-verify.js +36 -0
- package/src/commands/validate-receipt.js +38 -3
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +24 -0
- package/src/core/cli-command-definitions.js +163 -7
- package/src/core/cli-metadata.js +1 -1
- package/src/core/completion-artifacts.js +29 -3
- package/src/core/completion.js +101 -10
- package/src/core/diagnosis-model.js +214 -0
- package/src/core/diagnosis.js +171 -0
- package/src/core/error-codes.js +292 -0
- package/src/core/events.js +47 -1
- package/src/core/execution-prerequisites.js +38 -20
- package/src/core/execution.js +20 -3
- package/src/core/native-adapters.js +14 -4
- package/src/core/next-action-model.js +40 -5
- package/src/core/next-action.js +234 -91
- package/src/core/phase.js +29 -0
- package/src/core/policy-adapters.js +276 -0
- package/src/core/policy-baseline.js +144 -0
- package/src/core/policy-diff.js +133 -0
- package/src/core/policy-discovery.js +225 -0
- package/src/core/policy-engine.js +533 -0
- package/src/core/policy-mutation.js +139 -0
- package/src/core/preflight-consistency.js +23 -15
- package/src/core/preflight-model.js +10 -2
- package/src/core/preflight.js +65 -1
- package/src/core/progress.js +143 -0
- package/src/core/protocol.js +8 -0
- package/src/core/reconcile-closure.js +173 -0
- package/src/core/schema-validation.js +6 -0
- package/src/core/settlement-model.js +85 -0
- package/src/core/settlement.js +78 -0
- package/src/core/task-context.js +11 -0
- package/src/core/task-discovery.js +67 -1
- package/src/core/task-paths.js +9 -0
- package/src/core/templates.js +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical, declarative definition of all
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
381
|
-
name: "
|
|
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
|
-
|
|
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>/
|
|
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.",
|
package/src/core/cli-metadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Canonical metadata for all
|
|
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 (!
|
|
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" && !
|
|
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" && !
|
|
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
|
|
package/src/core/completion.js
CHANGED
|
@@ -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 "
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
case "
|
|
81
|
-
case "
|
|
82
|
-
return "
|
|
83
|
-
case "
|
|
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 ?
|
|
430
|
+
status: valid ? COMPLETION_STATUSES[0] : COMPLETION_STATUSES[1],
|
|
340
431
|
taskStatus: valid ? "COMPLETE" : receiptValue?.status === "blocked" ? "BLOCKED" : "INCOMPLETE",
|
|
341
|
-
verificationStatus: valid ?
|
|
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
|
+
}
|