@warpgogol/forge 5.3.4 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/cli.ts +7 -2
- package/dist/bin/cli.js +7 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +2 -0
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/audit/audit.module.d.ts.map +1 -1
- package/dist/os/audit/audit.module.js +2 -0
- package/dist/os/audit/audit.module.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +47 -15
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
- package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
- package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
- package/dist/os/compass/handlers/summary-record.d.ts +11 -2
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +95 -15
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +124 -73
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
- package/dist/os/core/handlers/file-size-lint.js +2 -0
- package/dist/os/core/handlers/file-size-lint.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +3 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/naming/naming-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +11 -1
- package/dist/os/naming/naming-convention.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +9 -2
- package/dist/os/naming/naming.module.js.map +1 -1
- package/dist/os/notes/notes.module.d.ts.map +1 -1
- package/dist/os/notes/notes.module.js +17 -6
- package/dist/os/notes/notes.module.js.map +1 -1
- package/dist/os/plan/plan.module.d.ts.map +1 -1
- package/dist/os/plan/plan.module.js +2 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts.map +1 -1
- package/dist/os/plugin/plugin.module.js +9 -2
- package/dist/os/plugin/plugin.module.js.map +1 -1
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +19 -4
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
- package/dist/os/queue/handlers/queue-validate.js +154 -7
- package/dist/os/queue/handlers/queue-validate.js.map +1 -1
- package/dist/os/queue/manifest.d.ts +17 -1
- package/dist/os/queue/manifest.d.ts.map +1 -1
- package/dist/os/queue/manifest.js +127 -6
- package/dist/os/queue/manifest.js.map +1 -1
- package/dist/os/queue/queue.module.d.ts.map +1 -1
- package/dist/os/queue/queue.module.js +32 -4
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/queue/types.d.ts +100 -1
- package/dist/os/queue/types.d.ts.map +1 -1
- package/dist/os/queue/types.js +44 -0
- package/dist/os/queue/types.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +30 -8
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +25 -6
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +12 -2
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +2 -2
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/rfc/verification-evidence.d.ts +21 -0
- package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
- package/dist/os/rfc/verification-evidence.js +134 -23
- package/dist/os/rfc/verification-evidence.js.map +1 -1
- package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
- package/dist/os/rfc/verification-refresh.js +66 -37
- package/dist/os/rfc/verification-refresh.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +42 -5
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +5 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
- package/dist/os/spec/live-spec-merge.js +55 -134
- package/dist/os/spec/live-spec-merge.js.map +1 -1
- package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
- package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
- package/dist/os/spec/live-spec-rebuild.js +272 -0
- package/dist/os/spec/live-spec-rebuild.js.map +1 -0
- package/dist/os/spec/live-spec-shared.d.ts +62 -0
- package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
- package/dist/os/spec/live-spec-shared.js +315 -0
- package/dist/os/spec/live-spec-shared.js.map +1 -0
- package/dist/os/spec/live-spec-types.d.ts +17 -1
- package/dist/os/spec/live-spec-types.d.ts.map +1 -1
- package/dist/os/spec/live-spec-types.js +3 -0
- package/dist/os/spec/live-spec-types.js.map +1 -1
- package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
- package/dist/os/spec/live-spec-validate.js +105 -2
- package/dist/os/spec/live-spec-validate.js.map +1 -1
- package/dist/os/spec/spec-materialize.d.ts +2 -0
- package/dist/os/spec/spec-materialize.d.ts.map +1 -1
- package/dist/os/spec/spec-materialize.js +45 -5
- package/dist/os/spec/spec-materialize.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +19 -0
- package/dist/os/spec/spec-schema.d.ts.map +1 -1
- package/dist/os/spec/spec-schema.js +30 -0
- package/dist/os/spec/spec-schema.js.map +1 -1
- package/dist/os/spec/spec-status.d.ts +4 -0
- package/dist/os/spec/spec-status.d.ts.map +1 -1
- package/dist/os/spec/spec-status.js +19 -1
- package/dist/os/spec/spec-status.js.map +1 -1
- package/dist/os/spec/spec-validate.d.ts.map +1 -1
- package/dist/os/spec/spec-validate.js +55 -8
- package/dist/os/spec/spec-validate.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +84 -8
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +13 -4
- package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
- package/dist/os/workflow/workflow.module.d.ts.map +1 -1
- package/dist/os/workflow/workflow.module.js +13 -5
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/cli-flags.d.ts.map +1 -1
- package/dist/src/cli-flags.js +25 -5
- package/dist/src/cli-flags.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +3 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +2 -3
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +8 -3
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +14 -1
- package/dist/src/pipeline-status.d.ts.map +1 -1
- package/dist/src/pipeline-status.js +28 -8
- package/dist/src/pipeline-status.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/generated-marker.d.ts +8 -0
- package/dist/src/utils/generated-marker.d.ts.map +1 -1
- package/dist/src/utils/generated-marker.js +25 -0
- package/dist/src/utils/generated-marker.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +2 -4
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/os/adr/adr.module.ts +2 -0
- package/os/audit/audit.module.ts +2 -0
- package/os/compass/compass.module.ts +49 -15
- package/os/compass/handlers/compass-audit-handler.ts +51 -10
- package/os/compass/handlers/compass-docs-validate.ts +361 -0
- package/os/compass/handlers/summary-record.ts +106 -15
- package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
- package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
- package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
- package/os/compass/handlers/tests/summary-record.test.ts +127 -0
- package/os/core/core.module.ts +147 -75
- package/os/core/docs-archive.test.ts +136 -0
- package/os/core/handlers/file-size-lint.ts +2 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/naming/naming-convention.ts +11 -1
- package/os/naming/naming.module.ts +9 -2
- package/os/notes/notes.module.ts +17 -6
- package/os/plan/plan-0000-template.md +150 -0
- package/os/plan/plan.module.ts +2 -0
- package/os/plugin/plugin.module.ts +9 -2
- package/os/program/program.module.ts +19 -4
- package/os/queue/decision-ledger.test.ts +163 -0
- package/os/queue/handlers/queue-validate.ts +190 -8
- package/os/queue/manifest.ts +201 -7
- package/os/queue/queue-validate.test.ts +303 -0
- package/os/queue/queue.module.ts +32 -4
- package/os/queue/types.ts +56 -1
- package/os/rfc/handlers/implement-stamp.ts +37 -8
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
- package/os/rfc/rfc.module.ts +27 -6
- package/os/rfc/types.ts +18 -3
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +149 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +74 -38
- package/os/session/handlers/save.ts +37 -5
- package/os/session/session.module.ts +5 -0
- package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
- package/os/spec/live-spec-list-show-validate.test.ts +10 -6
- package/os/spec/live-spec-merge.test.ts +187 -7
- package/os/spec/live-spec-merge.ts +71 -155
- package/os/spec/live-spec-rebuild.test.ts +317 -0
- package/os/spec/live-spec-rebuild.ts +356 -0
- package/os/spec/live-spec-shared.ts +378 -0
- package/os/spec/live-spec-types.ts +21 -1
- package/os/spec/live-spec-validate.test.ts +317 -0
- package/os/spec/live-spec-validate.ts +106 -2
- package/os/spec/spec-materialize.test.ts +192 -0
- package/os/spec/spec-materialize.ts +48 -4
- package/os/spec/spec-schema.ts +49 -0
- package/os/spec/spec-status.ts +24 -0
- package/os/spec/spec-validate.test.ts +66 -0
- package/os/spec/spec-validate.ts +62 -8
- package/os/spec/spec.module.ts +85 -8
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
- package/os/werkstatt/werkstatt.module.ts +13 -4
- package/os/workflow/workflow.module.ts +13 -5
- package/package.json +1 -1
- package/skills/_shared/fo-pipeline-conventions.md +126 -1
- package/skills/fo/fo-doc-audit/SKILL.md +4 -4
- package/skills/fo/fo-explore/SKILL.md +2 -1
- package/skills/fo/fo-fix/SKILL.md +11 -3
- package/skills/fo/fo-handoff/SKILL.md +4 -6
- package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
- package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
- package/skills/fo/fo-idea-implement/SKILL.md +53 -10
- package/skills/fo/fo-idea-plan/SKILL.md +28 -6
- package/skills/fo/fo-review/SKILL.md +69 -8
- package/skills/fo/fo-session-retro/SKILL.md +3 -2
- package/skills/fo/fo-step-commit/SKILL.md +2 -0
- package/skills/shared/grilling/SKILL.md +12 -0
- package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
- package/skills/shared/writing-great-skills/SKILL.md +2 -1
- package/src/cli-flags.ts +23 -5
- package/src/config/forge-config.ts +4 -0
- package/src/onboarding/doctor.ts +2 -3
- package/src/onboarding/init.ts +8 -3
- package/src/pipeline-status.ts +32 -8
- package/src/tests/cli-flags.test.ts +20 -0
- package/src/tests/cli-output.test.ts +13 -7
- package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
- package/src/tests/implement-stamp.test.ts +38 -0
- package/src/tests/session-handlers.test.ts +33 -0
- package/src/types.ts +13 -1
- package/src/utils/generated-marker.ts +28 -0
- package/src/validators/skill-validate.ts +2 -4
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
import { decisionLedgerPath, loadDecisionLedger } from "./manifest.ts";
|
|
7
|
+
import { decisionLedgerSchema } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "decision-ledger-test-"));
|
|
13
|
+
await fs.mkdir(path.join(tmpDir, "docs/queues"), { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("decisionLedgerSchema (RFC-1250)", () => {
|
|
21
|
+
test("round-trips the RFC example shape", () => {
|
|
22
|
+
const ledger = decisionLedgerSchema.parse({
|
|
23
|
+
id: "block-a.decisions",
|
|
24
|
+
queue: "block-a",
|
|
25
|
+
createdAt: "2026-10-09",
|
|
26
|
+
policies: [
|
|
27
|
+
{
|
|
28
|
+
id: "P-1",
|
|
29
|
+
question: "Blocking or advisory placement for new validators?",
|
|
30
|
+
answer: "blocking (build.check)",
|
|
31
|
+
appliesTo: "*",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
items: [
|
|
35
|
+
{
|
|
36
|
+
id: "Q-1",
|
|
37
|
+
doc: "RFC-1243",
|
|
38
|
+
stage: "enhance",
|
|
39
|
+
question: "Which storage backend?",
|
|
40
|
+
resolutionPath: "none",
|
|
41
|
+
options: [
|
|
42
|
+
{ label: "localStorage", recommended: true },
|
|
43
|
+
{ label: "unstorage", consequence: "server-side only" },
|
|
44
|
+
],
|
|
45
|
+
status: "open",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect(ledger.items).toHaveLength(1);
|
|
50
|
+
expect(ledger.items[0]?.status).toBe("open");
|
|
51
|
+
expect(ledger.policies).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("status enum rejects unknown values", () => {
|
|
55
|
+
const result = decisionLedgerSchema.safeParse({
|
|
56
|
+
id: "x.decisions",
|
|
57
|
+
queue: "x",
|
|
58
|
+
createdAt: "2026-10-09",
|
|
59
|
+
items: [
|
|
60
|
+
{
|
|
61
|
+
id: "Q-1",
|
|
62
|
+
doc: "RFC-1243",
|
|
63
|
+
stage: "enhance",
|
|
64
|
+
question: "q",
|
|
65
|
+
resolutionPath: "none",
|
|
66
|
+
status: "parked", // not a status — parked is a flag
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
expect(result.success).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("entry id must match Q-N", () => {
|
|
74
|
+
const result = decisionLedgerSchema.safeParse({
|
|
75
|
+
id: "x.decisions",
|
|
76
|
+
queue: "x",
|
|
77
|
+
createdAt: "2026-10-09",
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
id: "q1",
|
|
81
|
+
doc: "RFC-1243",
|
|
82
|
+
stage: "enhance",
|
|
83
|
+
question: "q",
|
|
84
|
+
resolutionPath: "none",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
expect(result.success).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("defaults: empty policies/items, status open, options []", () => {
|
|
92
|
+
const ledger = decisionLedgerSchema.parse({
|
|
93
|
+
id: "x.decisions",
|
|
94
|
+
queue: "x",
|
|
95
|
+
createdAt: "2026-10-09",
|
|
96
|
+
items: [
|
|
97
|
+
{
|
|
98
|
+
id: "Q-1",
|
|
99
|
+
doc: "RFC-1243",
|
|
100
|
+
stage: "plan",
|
|
101
|
+
question: "q",
|
|
102
|
+
resolutionPath: "codebase",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
expect(ledger.policies).toEqual([]);
|
|
107
|
+
expect(ledger.items[0]?.status).toBe("open");
|
|
108
|
+
expect(ledger.items[0]?.options).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("decisionLedgerPath / loadDecisionLedger (RFC-1250)", () => {
|
|
113
|
+
test("sibling path: <stem>.yaml → <stem>.decisions.yaml", () => {
|
|
114
|
+
expect(decisionLedgerPath("docs/queues/block-a.yaml")).toBe(
|
|
115
|
+
"docs/queues/block-a.decisions.yaml",
|
|
116
|
+
);
|
|
117
|
+
expect(decisionLedgerPath("docs/queues/block-a.yml")).toBe(
|
|
118
|
+
"docs/queues/block-a.decisions.yaml",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("absent ledger → null, no errors", async () => {
|
|
123
|
+
const { ledger, errors } = await loadDecisionLedger(tmpDir, "docs/queues/none.yaml");
|
|
124
|
+
expect(ledger).toBeNull();
|
|
125
|
+
expect(errors).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("valid ledger parses", async () => {
|
|
129
|
+
await fs.writeFile(
|
|
130
|
+
path.join(tmpDir, "docs/queues/block-a.yaml"),
|
|
131
|
+
"id: block-a\ncreatedAt: 2026-10-09\nitems: []\n",
|
|
132
|
+
);
|
|
133
|
+
await fs.writeFile(
|
|
134
|
+
path.join(tmpDir, "docs/queues/block-a.decisions.yaml"),
|
|
135
|
+
"id: block-a.decisions\nqueue: block-a\ncreatedAt: 2026-10-09\nitems:\n - id: Q-1\n doc: RFC-1243\n stage: enhance\n question: which?\n resolutionPath: none\n status: open\n",
|
|
136
|
+
);
|
|
137
|
+
const { ledger, errors } = await loadDecisionLedger(tmpDir, "docs/queues/block-a.yaml");
|
|
138
|
+
expect(errors).toEqual([]);
|
|
139
|
+
expect(ledger?.items).toHaveLength(1);
|
|
140
|
+
expect(ledger?.items[0]?.id).toBe("Q-1");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("invalid YAML → QUEUE-01 error on the manifest load path", async () => {
|
|
144
|
+
await fs.writeFile(
|
|
145
|
+
path.join(tmpDir, "docs/queues/block-b.decisions.yaml"),
|
|
146
|
+
"id: [unclosed\n",
|
|
147
|
+
);
|
|
148
|
+
const { ledger, errors } = await loadDecisionLedger(tmpDir, "docs/queues/block-b.yaml");
|
|
149
|
+
expect(ledger).toBeNull();
|
|
150
|
+
expect(errors.some((e) => e.ruleId === "QUEUE-01")).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("schema violation → QUEUE-01 error naming the ledger path", async () => {
|
|
154
|
+
await fs.writeFile(
|
|
155
|
+
path.join(tmpDir, "docs/queues/block-c.decisions.yaml"),
|
|
156
|
+
"id: block-c.decisions\nqueue: block-c\ncreatedAt: 2026-10-09\nitems:\n - id: BAD\n doc: RFC-1243\n stage: enhance\n question: q\n resolutionPath: none\n",
|
|
157
|
+
);
|
|
158
|
+
const { ledger, errors } = await loadDecisionLedger(tmpDir, "docs/queues/block-c.yaml");
|
|
159
|
+
expect(ledger).toBeNull();
|
|
160
|
+
expect(errors.some((e) => e.ruleId === "QUEUE-01")).toBe(true);
|
|
161
|
+
expect(errors[0]?.file).toBe("docs/queues/block-c.decisions.yaml");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
|
-
<purpose>queue.validate command handler (RFC-1140) — validates a
|
|
4
|
-
manifest and reports derived per-item
|
|
5
|
-
|
|
3
|
+
<purpose>queue.validate command handler (RFC-1140 + RFC-1250) — validates a
|
|
4
|
+
queue manifest and its decision-ledger sibling, reports derived per-item
|
|
5
|
+
pipeline status plus the next actionable item. QUEUE-07 blocks implementable
|
|
6
|
+
items carrying open decisions. Read-only; the orchestrator runs this before a
|
|
7
|
+
queued batch.</purpose>
|
|
6
8
|
<non-goals>
|
|
7
9
|
<item>Do not execute the queue — this command validates and previews only.</item>
|
|
8
10
|
<item>Do not mutate any files.</item>
|
|
9
11
|
</non-goals>
|
|
10
12
|
</MODULE_CONTRACT>
|
|
11
13
|
<CHANGE_SUMMARY>
|
|
14
|
+
<item>RFC-1250: decision-ledger wiring — QUEUE-07 open-decision gate on
|
|
15
|
+
executable items (un-parked opens only; parked items are already gated),
|
|
16
|
+
per-item decisions counts plus top-level totals, `next` excludes parked and
|
|
17
|
+
QUEUE-07-blocked items with dependsOn cascade.</item>
|
|
12
18
|
<item>RFC-1140: initial queue.validate handler.</item>
|
|
13
19
|
<item>RFC-1140: steps 1-4 — shared resolver, queue module, registration
|
|
14
20
|
|
|
15
21
|
Extract pipeline-status derivation into packages/forge/src/pipeline-status.ts, refactor rfc.pipeline.status onto it, add os/queue module with queue.validate command, register in WORKSHOP_MODULE_MAP.forge + bin/cli.ts + package.json exports.</item>
|
|
22
|
+
<item>RFC-1250: review wave — QUEUE-07 gates un-parked opens only, ledger binding checks, fail-closed loader
|
|
23
|
+
|
|
24
|
+
REVIEW-RFC-1250-01 findings: QUEUE-07 no longer fires on parked entries
|
|
25
|
+
(the park is the containment — a parked queue stays resumable and the
|
|
26
|
+
decision window arbitrates it); loader fails open only on ENOENT —
|
|
27
|
+
other read errors are QUEUE-01; ledger gains queue/id-stem binding
|
|
28
|
+
(QUEUE-02), Q-N uniqueness (QUEUE-05), and QUEUE-08 hygiene warnings
|
|
29
|
+
(missing answers, foreign doc ids); next excludes QUEUE-07-blocked
|
|
30
|
+
items; top-level decision totals added. Orchestrator pre-flight treats
|
|
31
|
+
QUEUE-07 as the window agenda — structural errors still stop the batch;
|
|
32
|
+
maturation skips parked/deferred items; uncovered imperative ask sites
|
|
33
|
+
gain collect riders (ADR code-trace, NC markers, audit-verdict guard).</item>
|
|
34
|
+
<item>RFC-1250: re-review wave — blocked dependsOn cascade, manifest-failure gating, generated artifacts</item>
|
|
16
35
|
</CHANGE_SUMMARY>
|
|
17
36
|
*/
|
|
18
37
|
|
|
@@ -22,8 +41,95 @@ import type {
|
|
|
22
41
|
ForgeRuntimeContext,
|
|
23
42
|
} from "../../../src/types.ts";
|
|
24
43
|
import { deriveQueueReport } from "../../../src/pipeline-status.ts";
|
|
25
|
-
import { loadQueueManifest } from "../manifest.ts";
|
|
26
|
-
import type { QueueValidateResult } from "../types.ts";
|
|
44
|
+
import { loadDecisionLedger, loadQueueManifest } from "../manifest.ts";
|
|
45
|
+
import type { DecisionEntry, DecisionLedger, QueueValidateResult } from "../types.ts";
|
|
46
|
+
|
|
47
|
+
interface DecisionSummary {
|
|
48
|
+
open: number;
|
|
49
|
+
/** Open entries WITHOUT parked: true — the QUEUE-07 blocking subset. */
|
|
50
|
+
openUnparked: number;
|
|
51
|
+
answered: number;
|
|
52
|
+
deferred: number;
|
|
53
|
+
autoResolved: number;
|
|
54
|
+
/** true when an open entry carries parked: true (hard-stop during execution). */
|
|
55
|
+
parked: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function summarizeDecisions(ledger: DecisionLedger | null): Map<string, DecisionSummary> {
|
|
59
|
+
const summaries = new Map<string, DecisionSummary>();
|
|
60
|
+
if (!ledger) return summaries;
|
|
61
|
+
for (const entry of ledger.items as DecisionEntry[]) {
|
|
62
|
+
const doc = entry.doc.toUpperCase();
|
|
63
|
+
const s = summaries.get(doc) ?? {
|
|
64
|
+
open: 0,
|
|
65
|
+
openUnparked: 0,
|
|
66
|
+
answered: 0,
|
|
67
|
+
deferred: 0,
|
|
68
|
+
autoResolved: 0,
|
|
69
|
+
parked: false,
|
|
70
|
+
};
|
|
71
|
+
if (entry.status === "open") {
|
|
72
|
+
s.open += 1;
|
|
73
|
+
if (entry.parked === true) {
|
|
74
|
+
s.parked = true;
|
|
75
|
+
} else {
|
|
76
|
+
s.openUnparked += 1;
|
|
77
|
+
}
|
|
78
|
+
} else if (entry.status === "answered") {
|
|
79
|
+
s.answered += 1;
|
|
80
|
+
} else if (entry.status === "deferred") {
|
|
81
|
+
s.deferred += 1;
|
|
82
|
+
} else if (entry.status === "auto-resolved") {
|
|
83
|
+
s.autoResolved += 1;
|
|
84
|
+
}
|
|
85
|
+
summaries.set(doc, s);
|
|
86
|
+
}
|
|
87
|
+
return summaries;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Fixed-point `dependsOn` cascade: a dependent of an excluded root is excluded. */
|
|
91
|
+
function cascadeDependents(
|
|
92
|
+
roots: ReadonlySet<string>,
|
|
93
|
+
dependsOn: Map<string, string[]>,
|
|
94
|
+
): Set<string> {
|
|
95
|
+
const excluded = new Set(roots);
|
|
96
|
+
let changed = true;
|
|
97
|
+
while (changed) {
|
|
98
|
+
changed = false;
|
|
99
|
+
for (const [itemId, deps] of dependsOn) {
|
|
100
|
+
if (excluded.has(itemId)) continue;
|
|
101
|
+
if (deps.some((dep) => excluded.has(dep))) {
|
|
102
|
+
excluded.add(itemId);
|
|
103
|
+
changed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return excluded;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* RFC-1250 parking: an item is parked when its ledger carries `deferred`
|
|
112
|
+
* entries (operator parked it at a window) or `open` entries with
|
|
113
|
+
* `parked: true` (system hard-stop during execution). An item whose in-queue
|
|
114
|
+
* `dependsOn` target is parked is itself parked — fixed-point cascade.
|
|
115
|
+
*/
|
|
116
|
+
function computeParkedDocIds(
|
|
117
|
+
itemIds: string[],
|
|
118
|
+
dependsOn: Map<string, string[]>,
|
|
119
|
+
summaries: Map<string, DecisionSummary>,
|
|
120
|
+
): Set<string> {
|
|
121
|
+
const roots = new Set<string>();
|
|
122
|
+
for (const id of itemIds) {
|
|
123
|
+
const s = summaries.get(id.toUpperCase());
|
|
124
|
+
if (s && (s.deferred > 0 || s.parked)) roots.add(id.toUpperCase());
|
|
125
|
+
}
|
|
126
|
+
return cascadeDependents(roots, dependsOn);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** QUEUE-07: an item derivable to the implement stage must not carry open decisions. */
|
|
130
|
+
function isImplementable(item: { status: string; pipelineStep?: string; kind?: string }): boolean {
|
|
131
|
+
return item.pipelineStep === "implement" || (item.kind === "adr" && item.status === "pending");
|
|
132
|
+
}
|
|
27
133
|
|
|
28
134
|
export async function runQueueValidate(
|
|
29
135
|
input: ForgeCommandInput,
|
|
@@ -36,9 +142,29 @@ export async function runQueueValidate(
|
|
|
36
142
|
throw new Error("queue.validate requires --file <path-to-manifest.yaml>");
|
|
37
143
|
}
|
|
38
144
|
|
|
39
|
-
const { manifest, errors, warnings } = await loadQueueManifest(workspaceRoot, file);
|
|
145
|
+
const { manifest, errors, warnings, dependsOn } = await loadQueueManifest(workspaceRoot, file);
|
|
146
|
+
|
|
147
|
+
// The ledger is part of the queue's durable state — its load errors block.
|
|
148
|
+
const {
|
|
149
|
+
ledger,
|
|
150
|
+
errors: ledgerErrors,
|
|
151
|
+
warnings: ledgerWarnings,
|
|
152
|
+
} = await loadDecisionLedger(workspaceRoot, file, manifest);
|
|
153
|
+
errors.push(...ledgerErrors);
|
|
154
|
+
warnings.push(...ledgerWarnings);
|
|
155
|
+
|
|
156
|
+
const summaries = summarizeDecisions(ledger);
|
|
157
|
+
const parkedIds = computeParkedDocIds(
|
|
158
|
+
manifest.items.map((i) => i.id),
|
|
159
|
+
dependsOn,
|
|
160
|
+
summaries,
|
|
161
|
+
);
|
|
40
162
|
|
|
41
|
-
|
|
163
|
+
// QUEUE-07: an executable item — implementable and not already parked —
|
|
164
|
+
// must not carry un-parked open decisions. Parked items are already
|
|
165
|
+
// excluded from execution by the park itself; their opens surface through
|
|
166
|
+
// the decision window's arbitration, not a second gate.
|
|
167
|
+
const derivable =
|
|
42
168
|
errors.length === 0
|
|
43
169
|
? await deriveQueueReport(
|
|
44
170
|
workspaceRoot,
|
|
@@ -46,15 +172,70 @@ export async function runQueueValidate(
|
|
|
46
172
|
)
|
|
47
173
|
: { items: [], next: null };
|
|
48
174
|
|
|
175
|
+
const blockedIds = new Set<string>();
|
|
176
|
+
for (const item of derivable.items) {
|
|
177
|
+
const s = summaries.get(item.id);
|
|
178
|
+
if (isImplementable(item) && !parkedIds.has(item.id) && s && s.openUnparked > 0) {
|
|
179
|
+
blockedIds.add(item.id);
|
|
180
|
+
errors.push({
|
|
181
|
+
ruleId: "QUEUE-07",
|
|
182
|
+
severity: "error",
|
|
183
|
+
message: `Item "${item.id}" is implementable but carries ${s.openUnparked} open decision(s) — resolve the decision window first`,
|
|
184
|
+
file,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Excluded from `next`: parked items, QUEUE-07-blocked items, and the
|
|
190
|
+
// dependsOn dependents of either — a dependent must not execute while its
|
|
191
|
+
// dependency sits unresolved behind the window.
|
|
192
|
+
const excludedFromNext = new Set([...parkedIds, ...cascadeDependents(blockedIds, dependsOn)]);
|
|
193
|
+
const report = {
|
|
194
|
+
items: derivable.items,
|
|
195
|
+
next:
|
|
196
|
+
derivable.items.find(
|
|
197
|
+
(i) =>
|
|
198
|
+
(i.status === "pending" || i.status === "in-progress") && !excludedFromNext.has(i.id),
|
|
199
|
+
)?.id ?? null,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
for (const item of report.items) {
|
|
203
|
+
const s = summaries.get(item.id);
|
|
204
|
+
if (s) {
|
|
205
|
+
item.decisions = {
|
|
206
|
+
open: s.open,
|
|
207
|
+
deferred: s.deferred,
|
|
208
|
+
autoResolved: s.autoResolved,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
49
213
|
const status = errors.length === 0 ? "pass" : "fail";
|
|
50
214
|
|
|
215
|
+
const decisionTotals = ledger
|
|
216
|
+
? {
|
|
217
|
+
open: ledger.items.filter((e) => e.status === "open").length,
|
|
218
|
+
answered: ledger.items.filter((e) => e.status === "answered").length,
|
|
219
|
+
deferred: ledger.items.filter((e) => e.status === "deferred").length,
|
|
220
|
+
autoResolved: ledger.items.filter((e) => e.status === "auto-resolved").length,
|
|
221
|
+
}
|
|
222
|
+
: undefined;
|
|
223
|
+
|
|
51
224
|
if (outputFormat === "pretty") {
|
|
52
225
|
logger.section(`Queue: ${manifest.id || file}`);
|
|
53
226
|
for (const item of report.items) {
|
|
54
227
|
const step = item.pipelineStep ? ` (${item.pipelineStep})` : "";
|
|
55
|
-
|
|
228
|
+
const decisions = item.decisions
|
|
229
|
+
? ` [decisions: ${item.decisions.open} open, ${item.decisions.deferred} deferred, ${item.decisions.autoResolved} auto-resolved]`
|
|
230
|
+
: "";
|
|
231
|
+
logger.info(` ${item.id}: ${item.status}${step}${decisions}`);
|
|
56
232
|
}
|
|
57
233
|
logger.info(`next: ${report.next ?? "—"}`);
|
|
234
|
+
if (decisionTotals) {
|
|
235
|
+
logger.info(
|
|
236
|
+
`decisions: ${decisionTotals.open} open, ${decisionTotals.answered} answered, ${decisionTotals.autoResolved} auto-resolved, ${decisionTotals.deferred} deferred`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
58
239
|
for (const w of warnings) {
|
|
59
240
|
logger.warn(` ${w.ruleId}: ${w.message}`);
|
|
60
241
|
}
|
|
@@ -71,6 +252,7 @@ export async function runQueueValidate(
|
|
|
71
252
|
file,
|
|
72
253
|
items: report.items,
|
|
73
254
|
next: report.next,
|
|
255
|
+
decisions: decisionTotals,
|
|
74
256
|
errors,
|
|
75
257
|
warnings,
|
|
76
258
|
},
|
package/os/queue/manifest.ts
CHANGED
|
@@ -2,17 +2,37 @@
|
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
3
|
<purpose>Queue manifest loading and validation (RFC-1140) — YAML parse, zod
|
|
4
4
|
schema, id↔filename stem check, item id pattern, document resolution across
|
|
5
|
-
active and archive dirs, duplicate detection, and dependsOn order warnings
|
|
5
|
+
active and archive dirs, duplicate detection, and dependsOn order warnings.
|
|
6
|
+
Also loads the decision-ledger sibling (RFC-1250).</purpose>
|
|
6
7
|
<non-goals>
|
|
7
8
|
<item>Do not derive item status — that is src/pipeline-status.ts.</item>
|
|
8
9
|
<item>Do not mutate the manifest file.</item>
|
|
9
10
|
</non-goals>
|
|
10
11
|
</MODULE_CONTRACT>
|
|
11
12
|
<CHANGE_SUMMARY>
|
|
13
|
+
<item>RFC-1250: loadDecisionLedger — sibling stem.decisions.yaml resolution,
|
|
14
|
+
ENOENT-only absence (other read errors are QUEUE-01 blocking), schema check,
|
|
15
|
+
ledger↔manifest binding (queue/id stem QUEUE-02, duplicate Q-N QUEUE-05),
|
|
16
|
+
hygiene warnings QUEUE-08 (missing answers, foreign doc ids);
|
|
17
|
+
LoadedQueueManifest exposes resolved dependsOn edges for the parking
|
|
18
|
+
cascade.</item>
|
|
12
19
|
<item>RFC-1140: initial manifest loader and validator.</item>
|
|
13
20
|
<item>RFC-1140: steps 1-4 — shared resolver, queue module, registration
|
|
14
21
|
|
|
15
22
|
Extract pipeline-status derivation into packages/forge/src/pipeline-status.ts, refactor rfc.pipeline.status onto it, add os/queue module with queue.validate command, register in WORKSHOP_MODULE_MAP.forge + bin/cli.ts + package.json exports.</item>
|
|
23
|
+
<item>RFC-1250: review wave — QUEUE-07 gates un-parked opens only, ledger binding checks, fail-closed loader
|
|
24
|
+
|
|
25
|
+
REVIEW-RFC-1250-01 findings: QUEUE-07 no longer fires on parked entries
|
|
26
|
+
(the park is the containment — a parked queue stays resumable and the
|
|
27
|
+
decision window arbitrates it); loader fails open only on ENOENT —
|
|
28
|
+
other read errors are QUEUE-01; ledger gains queue/id-stem binding
|
|
29
|
+
(QUEUE-02), Q-N uniqueness (QUEUE-05), and QUEUE-08 hygiene warnings
|
|
30
|
+
(missing answers, foreign doc ids); next excludes QUEUE-07-blocked
|
|
31
|
+
items; top-level decision totals added. Orchestrator pre-flight treats
|
|
32
|
+
QUEUE-07 as the window agenda — structural errors still stop the batch;
|
|
33
|
+
maturation skips parked/deferred items; uncovered imperative ask sites
|
|
34
|
+
gain collect riders (ADR code-trace, NC markers, audit-verdict guard).</item>
|
|
35
|
+
<item>RFC-1250: re-review wave — blocked dependsOn cascade, manifest-failure gating, generated artifacts</item>
|
|
16
36
|
</CHANGE_SUMMARY>
|
|
17
37
|
*/
|
|
18
38
|
|
|
@@ -22,13 +42,15 @@ import YAML from "yaml";
|
|
|
22
42
|
|
|
23
43
|
import type { Diagnostic } from "../../src/types.ts";
|
|
24
44
|
import { resolveDocument } from "../../src/pipeline-status.ts";
|
|
25
|
-
import { QUEUE_ITEM_ID_PATTERN, queueManifestSchema } from "./types.ts";
|
|
26
|
-
import type { QueueManifest } from "./types.ts";
|
|
45
|
+
import { QUEUE_ITEM_ID_PATTERN, decisionLedgerSchema, queueManifestSchema } from "./types.ts";
|
|
46
|
+
import type { DecisionLedger, QueueManifest } from "./types.ts";
|
|
27
47
|
|
|
28
48
|
export interface LoadedQueueManifest {
|
|
29
49
|
manifest: QueueManifest;
|
|
30
50
|
errors: Diagnostic[];
|
|
31
51
|
warnings: Diagnostic[];
|
|
52
|
+
/** In-queue dependsOn edges resolved from document frontmatter (RFC-1250 parking cascade). */
|
|
53
|
+
dependsOn: Map<string, string[]>;
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
function diag(
|
|
@@ -66,7 +88,12 @@ export async function loadQueueManifest(
|
|
|
66
88
|
displayPath,
|
|
67
89
|
),
|
|
68
90
|
);
|
|
69
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
manifest: { id: "", createdAt: "", items: [] },
|
|
93
|
+
errors,
|
|
94
|
+
warnings,
|
|
95
|
+
dependsOn: new Map(),
|
|
96
|
+
};
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
let raw: unknown;
|
|
@@ -81,7 +108,12 @@ export async function loadQueueManifest(
|
|
|
81
108
|
displayPath,
|
|
82
109
|
),
|
|
83
110
|
);
|
|
84
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
manifest: { id: "", createdAt: "", items: [] },
|
|
113
|
+
errors,
|
|
114
|
+
warnings,
|
|
115
|
+
dependsOn: new Map(),
|
|
116
|
+
};
|
|
85
117
|
}
|
|
86
118
|
|
|
87
119
|
const parsed = queueManifestSchema.safeParse(raw);
|
|
@@ -96,7 +128,12 @@ export async function loadQueueManifest(
|
|
|
96
128
|
),
|
|
97
129
|
);
|
|
98
130
|
}
|
|
99
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
manifest: { id: "", createdAt: "", items: [] },
|
|
133
|
+
errors,
|
|
134
|
+
warnings,
|
|
135
|
+
dependsOn: new Map(),
|
|
136
|
+
};
|
|
100
137
|
}
|
|
101
138
|
|
|
102
139
|
const manifest = parsed.data;
|
|
@@ -180,5 +217,162 @@ export async function loadQueueManifest(
|
|
|
180
217
|
}
|
|
181
218
|
}
|
|
182
219
|
|
|
183
|
-
return { manifest, errors, warnings };
|
|
220
|
+
return { manifest, errors, warnings, dependsOn: resolvedDependsOn };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Decision ledger (RFC-1250) — sibling <stem>.decisions.yaml of the manifest
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
export interface LoadedDecisionLedger {
|
|
228
|
+
ledger: DecisionLedger | null;
|
|
229
|
+
errors: Diagnostic[];
|
|
230
|
+
warnings: Diagnostic[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Manifest `docs/queues/<id>.yaml` → sibling ledger `docs/queues/<id>.decisions.yaml`. */
|
|
234
|
+
export function decisionLedgerPath(manifestPath: string): string {
|
|
235
|
+
return manifestPath.replace(/\.(ya?ml)$/i, ".decisions.yaml");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Load the decision ledger sibling of a queue manifest. An absent ledger is
|
|
240
|
+
* legal — it means zero recorded decisions. An unreadable or schema-invalid
|
|
241
|
+
* ledger joins the manifest load path as a QUEUE-01 blocking error: the ledger
|
|
242
|
+
* is part of the queue's durable state.
|
|
243
|
+
*/
|
|
244
|
+
export async function loadDecisionLedger(
|
|
245
|
+
workspaceRoot: string,
|
|
246
|
+
filePath: string,
|
|
247
|
+
manifest?: QueueManifest,
|
|
248
|
+
): Promise<LoadedDecisionLedger> {
|
|
249
|
+
const errors: Diagnostic[] = [];
|
|
250
|
+
const warnings: Diagnostic[] = [];
|
|
251
|
+
const ledgerPath = decisionLedgerPath(filePath);
|
|
252
|
+
const absolutePath = path.isAbsolute(ledgerPath)
|
|
253
|
+
? ledgerPath
|
|
254
|
+
: path.join(workspaceRoot, ledgerPath);
|
|
255
|
+
const displayPath = path.isAbsolute(ledgerPath)
|
|
256
|
+
? path.relative(workspaceRoot, ledgerPath)
|
|
257
|
+
: ledgerPath;
|
|
258
|
+
|
|
259
|
+
let source: string;
|
|
260
|
+
try {
|
|
261
|
+
source = await fs.readFile(absolutePath);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// ENOENT = legal absence (zero recorded decisions). Any other read
|
|
264
|
+
// failure (EACCES, EISDIR, …) is an unreadable durable artifact — the
|
|
265
|
+
// ledger must never fail open into "no decisions" or QUEUE-07 disarms.
|
|
266
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") {
|
|
267
|
+
return { ledger: null, errors, warnings };
|
|
268
|
+
}
|
|
269
|
+
errors.push(
|
|
270
|
+
diag(
|
|
271
|
+
"QUEUE-01",
|
|
272
|
+
"error",
|
|
273
|
+
`Cannot read decision ledger: ${e instanceof Error ? e.message : String(e)}`,
|
|
274
|
+
displayPath,
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
return { ledger: null, errors, warnings };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let raw: unknown;
|
|
281
|
+
try {
|
|
282
|
+
raw = YAML.parse(source);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
errors.push(
|
|
285
|
+
diag(
|
|
286
|
+
"QUEUE-01",
|
|
287
|
+
"error",
|
|
288
|
+
`Decision ledger YAML parse error: ${e instanceof Error ? e.message : String(e)}`,
|
|
289
|
+
displayPath,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
return { ledger: null, errors, warnings };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const parsed = decisionLedgerSchema.safeParse(raw);
|
|
296
|
+
if (!parsed.success) {
|
|
297
|
+
for (const issue of parsed.error.issues) {
|
|
298
|
+
errors.push(
|
|
299
|
+
diag(
|
|
300
|
+
"QUEUE-01",
|
|
301
|
+
"error",
|
|
302
|
+
`Decision ledger schema violation at ${issue.path.join(".") || "<root>"}: ${issue.message}`,
|
|
303
|
+
displayPath,
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return { ledger: null, errors, warnings };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ledger = parsed.data;
|
|
311
|
+
|
|
312
|
+
// Binding and hygiene checks require a successfully loaded manifest — when
|
|
313
|
+
// the manifest itself failed, the stub (empty id, no items) would blame a
|
|
314
|
+
// healthy ledger for a broken queue (QUEUE-02 against "") and flag every
|
|
315
|
+
// entry as foreign (QUEUE-08). The manifest failure already blocks.
|
|
316
|
+
const manifestId = manifest?.id ?? "";
|
|
317
|
+
const manifestReady = manifestId !== "";
|
|
318
|
+
|
|
319
|
+
// QUEUE-02-family binding: the ledger belongs to exactly one manifest —
|
|
320
|
+
// `queue` names the manifest id and `id` names the ledger filename stem
|
|
321
|
+
// (`<manifest-stem>.decisions`). A misplaced or copy-pasted ledger must not
|
|
322
|
+
// silently apply foreign decisions to this queue.
|
|
323
|
+
const ledgerStem = path.basename(absolutePath).replace(/\.(ya?ml)$/i, "");
|
|
324
|
+
if (manifestReady && ledger.queue !== manifestId) {
|
|
325
|
+
errors.push(
|
|
326
|
+
diag(
|
|
327
|
+
"QUEUE-02",
|
|
328
|
+
"error",
|
|
329
|
+
`Decision ledger queue "${ledger.queue}" does not match manifest id "${manifestId}"`,
|
|
330
|
+
displayPath,
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (ledger.id !== ledgerStem) {
|
|
335
|
+
errors.push(
|
|
336
|
+
diag(
|
|
337
|
+
"QUEUE-02",
|
|
338
|
+
"error",
|
|
339
|
+
`Decision ledger id "${ledger.id}" does not match filename stem "${ledgerStem}"`,
|
|
340
|
+
displayPath,
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// QUEUE-05-family: decision ids are queue-global — window codes and
|
|
346
|
+
// PENDING DECISION markers address them, so duplicates corrupt arbitration.
|
|
347
|
+
const seenDecisions = new Set<string>();
|
|
348
|
+
const manifestDocs = new Set((manifest?.items ?? []).map((i) => i.id.trim().toUpperCase()));
|
|
349
|
+
for (const entry of ledger.items) {
|
|
350
|
+
if (seenDecisions.has(entry.id)) {
|
|
351
|
+
errors.push(diag("QUEUE-05", "error", `Duplicate decision id "${entry.id}"`, displayPath));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
seenDecisions.add(entry.id);
|
|
355
|
+
if ((entry.status === "answered" || entry.status === "auto-resolved") && !entry.answer) {
|
|
356
|
+
warnings.push(
|
|
357
|
+
diag(
|
|
358
|
+
"QUEUE-08",
|
|
359
|
+
"warning",
|
|
360
|
+
`Decision "${entry.id}" is ${entry.status} but carries no answer — implement has nothing to bind`,
|
|
361
|
+
displayPath,
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (manifestReady && !manifestDocs.has(entry.doc.toUpperCase())) {
|
|
366
|
+
warnings.push(
|
|
367
|
+
diag(
|
|
368
|
+
"QUEUE-08",
|
|
369
|
+
"warning",
|
|
370
|
+
`Decision "${entry.id}" targets "${entry.doc}" which is not a manifest item — invisible to this queue`,
|
|
371
|
+
displayPath,
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { ledger, errors, warnings };
|
|
184
378
|
}
|