vibe-coding-master 0.8.12 → 0.9.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/README.md +7 -3
- package/dist/backend/cli/install-vcm-harness.js +57 -18
- package/dist/backend/gateway/gateway-service.js +5 -3
- package/dist/backend/runtime/terminal-submit.js +1 -0
- package/dist/backend/server.js +14 -4
- package/dist/backend/services/app-settings-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +6 -4
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +2 -13
- package/dist/backend/services/claude-version-check.js +46 -0
- package/dist/backend/services/gate-review-service.js +47 -8
- package/dist/backend/services/harness-service.js +41 -20
- package/dist/backend/services/message-service.js +30 -6
- package/dist/backend/services/pm-user-question.js +2 -1
- package/dist/backend/services/project-service.js +8 -2
- package/dist/backend/services/runtime-recovery-service.js +36 -9
- package/dist/backend/services/session-service.js +10 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-process-exit-service.js +4 -1
- package/dist/backend/services/workflow-control-service.js +55 -11
- package/dist/backend/templates/harness/architect-agent.js +2 -1
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +2 -1
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +8 -2
- package/dist/backend/templates/harness/architect-validation-worker-agent.js +2 -1
- package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
- package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
- package/dist/backend/templates/harness/coder-worker-agent.js +1 -0
- package/dist/backend/templates/harness/gate-review.js +676 -643
- package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
- package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -0
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +6 -2
- package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
- package/dist/shared/types/app-settings.js +31 -0
- package/dist-frontend/assets/index-CShYYQQa.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.js} +41 -41
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +332 -298
- package/scripts/harness-tools/generate-module-index +1047 -512
- package/scripts/harness-tools/generate-public-surface +1231 -1069
- package/scripts/harness-tools/package.json +3 -0
- package/scripts/harness-tools/run-long-check +608 -531
- package/scripts/harness-tools/vcm-artifact +181 -113
- package/scripts/harness-tools/vcm-bash-guard +839 -605
- package/scripts/harness-tools/vcm-subagent-guard +67 -63
- package/scripts/harness-tools/watch-job +313 -238
- package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
|
@@ -1,516 +1,485 @@
|
|
|
1
|
+
import { NODE_TOOL_ARGUMENT_PARSER, renderNodeToolHeader } from "./node-tool-runtime.js";
|
|
1
2
|
export function renderCheckScaffoldLedgerTool() {
|
|
2
|
-
return
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
The architecture plan's Scaffold Manifest is an item ledger: one entry per implementation
|
|
6
|
-
item, one unique ID per entry, and exactly one
|
|
7
|
-
|
|
8
|
-
markers and preserves failed markers. The required
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
The tool checks:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Markers are scanned in git-tracked source files only (
|
|
26
|
-
prose may quote markers legitimately). Pure read; findings go to stderr; exit 0 clean,
|
|
27
|
-
1 on findings.
|
|
28
|
-
locations. No plan file at all means there is nothing to check (exit 0)
|
|
29
|
-
planning-free task.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
3
|
+
return String.raw `${renderNodeToolHeader("Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.")}${NODE_TOOL_ARGUMENT_PARSER}
|
|
4
|
+
// Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.
|
|
5
|
+
//
|
|
6
|
+
// The architecture plan's Scaffold Manifest is an item ledger: one entry per implementation
|
|
7
|
+
// item, one unique ID per entry, and exactly one "VCM:CODE <ID>" marker in the tree per
|
|
8
|
+
// create/change/delete entry during scaffolding. Coder then removes successful
|
|
9
|
+
// markers and preserves failed markers. The required --mode selects that lifecycle:
|
|
10
|
+
//
|
|
11
|
+
// - scaffold: require the ledger and tree markers to form an exact bijection;
|
|
12
|
+
// - completion: reconcile the ledger, Coder's Scaffold Completion table, and the
|
|
13
|
+
// final tree marker state.
|
|
14
|
+
//
|
|
15
|
+
// The tool checks:
|
|
16
|
+
//
|
|
17
|
+
// 1. the ledger header follows the mandated column order (ID | Action | File | ...),
|
|
18
|
+
// every data-row ID matches the documented grammar as a complete cell and is unique,
|
|
19
|
+
// classified by its whole-cell action column
|
|
20
|
+
// (create/change/delete), and bound to a declared file path in the file column;
|
|
21
|
+
// 2. the ledger ID set equals the tree marker ID set, each ID exactly once on each side;
|
|
22
|
+
// 3. every marker sits in its entry's declared file;
|
|
23
|
+
// 4. the manifest contains no open-ended coverage language ("as work proceeds",
|
|
24
|
+
// "replicate", "etc.", "and others").
|
|
25
|
+
//
|
|
26
|
+
// Markers are scanned in git-tracked source files only (.md files and .ai/ are excluded:
|
|
27
|
+
// prose may quote markers legitimately). Pure read; findings go to stderr; exit 0 clean,
|
|
28
|
+
// 1 on findings. --plan <path> and --completion <path> override the default artifact
|
|
29
|
+
// locations. No plan file at all means there is nothing to check (exit 0) - a docs-only or
|
|
30
|
+
// planning-free task.
|
|
31
|
+
|
|
32
|
+
const childProcess = require("node:child_process");
|
|
33
|
+
const fs = require("node:fs");
|
|
34
|
+
const path = require("node:path");
|
|
35
|
+
|
|
36
|
+
const B = "\x60";
|
|
37
|
+
const PLAN = ".ai/vcm/handoffs/architecture-plan.md";
|
|
38
|
+
const COMPLETION = ".ai/vcm/handoffs/coder-completion.md";
|
|
39
|
+
const MANIFEST_HEADING = /^##\s+Scaffold Manifest\s*$/;
|
|
40
|
+
const COMPLETION_HEADING = /^##\s+Scaffold Completion\s*$/;
|
|
41
|
+
const SECTION_HEADING = /^##\s+\S/;
|
|
42
|
+
const ID_PATTERN = /^[A-Z]{2,6}-\d{1,4}$/;
|
|
43
|
+
const EMPTY_MANIFEST = "No scaffold items.";
|
|
44
|
+
const TABLE_SEPARATOR_CELL = /^:?-{3,}:?$/;
|
|
45
|
+
// Mandated column order: ID | action | file | symbol/site | work | freedom | proof.
|
|
46
|
+
// The action is read from its own cell as a whole-cell verb - never sniffed from the
|
|
47
|
+
// row text, so paths or prose containing action words cannot flip an item's class.
|
|
48
|
+
const ACTIONS = new Set(["create", "change", "delete"]);
|
|
49
|
+
const RESULTS = new Set(["done", "failed"]);
|
|
50
|
+
const MARKER_STATES = new Set(["removed", "present"]);
|
|
51
|
+
const PATH_TOKEN = /\x60([^\x60\s]+\/[^\x60\s]+|[^\x60\s]+\.[A-Za-z0-9]{1,8})\x60/;
|
|
52
|
+
const MARKER_ANY = /VCM:CODE(?![A-Za-z0-9_])/g;
|
|
53
|
+
const FORBIDDEN = [
|
|
54
|
+
[/as work proceeds/i, "as work proceeds"],
|
|
55
|
+
[/\breplicate\b/i, "\\breplicate\\b"],
|
|
56
|
+
[/\betc\./i, "\\betc\\."],
|
|
57
|
+
[/\band others\b/i, "\\band others\\b"]
|
|
58
|
+
];
|
|
59
|
+
const ID_GRAMMAR = B + "[A-Z]{2,6}-[0-9]{1,4}" + B;
|
|
60
|
+
|
|
61
|
+
function splitLines(text) {
|
|
62
|
+
return text.split(/\r\n|\n|\r/).filter((line, index, all) => index < all.length - 1 || line !== "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function section(text, heading) {
|
|
66
|
+
const lines = splitLines(text);
|
|
67
|
+
let start = null;
|
|
68
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
69
|
+
if (start === null) {
|
|
70
|
+
if (heading.test(lines[index])) {
|
|
71
|
+
start = index + 1;
|
|
72
|
+
}
|
|
73
|
+
} else if (SECTION_HEADING.test(lines[index])) {
|
|
74
|
+
return { start, lines: lines.slice(start, index) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return start === null ? null : { start, lines: lines.slice(start) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tableCells(stripped) {
|
|
81
|
+
return stripped.replace(/^\|+|\|+$/g, "").split("|").map((cell) => cell.trim());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSeparatorRow(cells) {
|
|
85
|
+
return cells.length > 0 && cells.every((cell) => TABLE_SEPARATOR_CELL.test(cell.replaceAll(" ", "")));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseLedger(sectionStart, lines, plan) {
|
|
89
|
+
const entries = new Map();
|
|
90
|
+
const findings = [];
|
|
91
|
+
let headerLine = null;
|
|
92
|
+
let separatorSeen = false;
|
|
93
|
+
let explicitEmptyLine = null;
|
|
94
|
+
lines.forEach((line, offset) => {
|
|
95
|
+
const lineNo = sectionStart + offset + 1;
|
|
96
|
+
const stripped = line.trim();
|
|
97
|
+
if (stripped === EMPTY_MANIFEST) {
|
|
98
|
+
if (explicitEmptyLine !== null) {
|
|
99
|
+
findings.push(plan + ":" + lineNo + " [ledger] duplicate " + B + EMPTY_MANIFEST + B + " declaration "
|
|
100
|
+
+ "(first at line " + explicitEmptyLine + ")");
|
|
101
|
+
} else {
|
|
102
|
+
explicitEmptyLine = lineNo;
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!stripped.startsWith("|")) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const cells = tableCells(stripped);
|
|
110
|
+
|
|
111
|
+
if (headerLine === null) {
|
|
112
|
+
headerLine = lineNo;
|
|
113
|
+
const head = cells.slice(0, 3).map((cell) => cell.toLowerCase());
|
|
114
|
+
if (head.join("|") !== "id|action|file" || head.length !== 3) {
|
|
115
|
+
findings.push(plan + ":" + lineNo + " [ledger] header columns must begin exactly "
|
|
116
|
+
+ B + "ID | Action | File | ..." + B + " -> fix the ledger column order");
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!separatorSeen) {
|
|
122
|
+
if (isSeparatorRow(cells)) {
|
|
123
|
+
separatorSeen = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
findings.push(plan + ":" + lineNo + " [ledger] missing Markdown separator row after "
|
|
127
|
+
+ "the Scaffold Manifest header");
|
|
128
|
+
separatorSeen = true;
|
|
129
|
+
} else if (isSeparatorRow(cells)) {
|
|
130
|
+
findings.push(plan + ":" + lineNo + " [ledger] unexpected separator row inside Scaffold Manifest data");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const idCell = cells.length > 0 ? cells[0] : "";
|
|
135
|
+
if (!ID_PATTERN.test(idCell)) {
|
|
136
|
+
findings.push(plan + ":" + lineNo + " [ledger] ID cell " + B + idCell + B + " must match " + ID_GRAMMAR + " exactly");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (entries.has(idCell)) {
|
|
140
|
+
findings.push(plan + ":" + lineNo + " [ledger] duplicate ledger ID " + idCell + " "
|
|
141
|
+
+ "(first at line " + entries.get(idCell).line + ") -> one entry per item");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const actionCell = cells.length > 1 ? cells[1].toLowerCase() : "";
|
|
145
|
+
const action = ACTIONS.has(actionCell) ? actionCell : null;
|
|
146
|
+
let declared = null;
|
|
147
|
+
if (cells.length > 2) {
|
|
148
|
+
const pathMatch = PATH_TOKEN.exec(cells[2]);
|
|
149
|
+
declared = pathMatch ? pathMatch[1] : null;
|
|
150
|
+
}
|
|
151
|
+
if (action === null) {
|
|
152
|
+
findings.push(plan + ":" + lineNo + " [ledger] " + idCell + " action column is not exactly "
|
|
153
|
+
+ "one of create/change/delete -> classify the item in column 2");
|
|
154
|
+
}
|
|
155
|
+
if (declared === null) {
|
|
156
|
+
findings.push(plan + ":" + lineNo + " [ledger] " + idCell + " has no declared file path in "
|
|
157
|
+
+ "column 3 -> bind the item to its exact file");
|
|
158
|
+
}
|
|
159
|
+
entries.set(idCell, { path: declared, action, line: lineNo });
|
|
160
|
+
});
|
|
161
|
+
if (explicitEmptyLine !== null) {
|
|
162
|
+
if (headerLine !== null) {
|
|
163
|
+
findings.push(plan + ":" + explicitEmptyLine + " [ledger] " + B + EMPTY_MANIFEST + B + " cannot be "
|
|
164
|
+
+ "combined with a Scaffold Manifest table");
|
|
165
|
+
}
|
|
166
|
+
return { entries, findings, explicitEmpty: true };
|
|
167
|
+
}
|
|
168
|
+
if (headerLine === null) {
|
|
169
|
+
findings.push(plan + ":" + (sectionStart + 1) + " [ledger] Scaffold Manifest must contain the "
|
|
170
|
+
+ "required table or the exact line " + B + EMPTY_MANIFEST + B);
|
|
171
|
+
return { entries, findings, explicitEmpty: false };
|
|
172
|
+
}
|
|
173
|
+
if (!separatorSeen) {
|
|
174
|
+
findings.push(plan + ":" + headerLine + " [ledger] Scaffold Manifest table is missing its separator row");
|
|
175
|
+
}
|
|
176
|
+
if (entries.size === 0) {
|
|
177
|
+
findings.push(plan + ":" + headerLine + " [ledger] Scaffold Manifest contains no valid entries; "
|
|
178
|
+
+ "use the exact line " + B + EMPTY_MANIFEST + B + " only when no scaffold item exists");
|
|
179
|
+
}
|
|
180
|
+
return { entries, findings, explicitEmpty: false };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseCompletion(sectionStart, lines, completion) {
|
|
184
|
+
const entries = new Map();
|
|
185
|
+
const findings = [];
|
|
186
|
+
let headerLine = null;
|
|
187
|
+
let separatorSeen = false;
|
|
188
|
+
lines.forEach((line, offset) => {
|
|
189
|
+
const lineNo = sectionStart + offset + 1;
|
|
190
|
+
const stripped = line.trim();
|
|
191
|
+
if (!stripped.startsWith("|")) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const cells = tableCells(stripped);
|
|
195
|
+
|
|
196
|
+
if (headerLine === null) {
|
|
197
|
+
headerLine = lineNo;
|
|
198
|
+
if (cells.map((cell) => cell.toLowerCase()).join("|") !== "id|action|result|marker state|proof evidence") {
|
|
199
|
+
findings.push(completion + ":" + lineNo + " [completion] header columns must be exactly "
|
|
200
|
+
+ B + "ID | Action | Result | Marker State | Proof Evidence" + B);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!separatorSeen) {
|
|
206
|
+
if (isSeparatorRow(cells)) {
|
|
207
|
+
separatorSeen = true;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
findings.push(completion + ":" + lineNo + " [completion] missing Markdown separator row after "
|
|
211
|
+
+ "the Scaffold Completion header");
|
|
212
|
+
separatorSeen = true;
|
|
213
|
+
} else if (isSeparatorRow(cells)) {
|
|
214
|
+
findings.push(completion + ":" + lineNo + " [completion] unexpected separator row inside "
|
|
215
|
+
+ "Scaffold Completion data");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const entryId = cells.length > 0 ? cells[0] : "";
|
|
220
|
+
if (!ID_PATTERN.test(entryId)) {
|
|
221
|
+
findings.push(completion + ":" + lineNo + " [completion] ID cell " + B + entryId + B + " must match " + ID_GRAMMAR + " exactly");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (entries.has(entryId)) {
|
|
225
|
+
findings.push(completion + ":" + lineNo + " [completion] duplicate ID " + entryId + " "
|
|
226
|
+
+ "(first at line " + entries.get(entryId).line + ")");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const action = cells.length > 1 ? cells[1].toLowerCase() : "";
|
|
231
|
+
const result = cells.length > 2 ? cells[2].toLowerCase() : "";
|
|
232
|
+
const markerState = cells.length > 3 ? cells[3].toLowerCase() : "";
|
|
233
|
+
if (!ACTIONS.has(action)) {
|
|
234
|
+
findings.push(completion + ":" + lineNo + " [completion] " + entryId + " action must be exactly create/change/delete");
|
|
235
|
+
}
|
|
236
|
+
if (!RESULTS.has(result)) {
|
|
237
|
+
findings.push(completion + ":" + lineNo + " [completion] " + entryId + " result must be exactly done/failed");
|
|
238
|
+
}
|
|
239
|
+
if (!MARKER_STATES.has(markerState)) {
|
|
240
|
+
findings.push(completion + ":" + lineNo + " [completion] " + entryId + " Marker State must be exactly removed/present");
|
|
241
|
+
}
|
|
242
|
+
entries.set(entryId, { action, result, markerState, line: lineNo });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (headerLine === null) {
|
|
246
|
+
findings.push(completion + ":" + (sectionStart + 1) + " [completion] Scaffold Completion must "
|
|
247
|
+
+ "contain the required table");
|
|
248
|
+
} else if (!separatorSeen) {
|
|
249
|
+
findings.push(completion + ":" + headerLine + " [completion] Scaffold Completion table is "
|
|
250
|
+
+ "missing its separator row");
|
|
251
|
+
}
|
|
252
|
+
if (entries.size === 0) {
|
|
253
|
+
findings.push(completion + ":" + (headerLine || sectionStart + 1) + " [completion] Scaffold "
|
|
254
|
+
+ "Completion contains no valid entries");
|
|
255
|
+
}
|
|
256
|
+
return { entries, findings };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function completionDecision(text) {
|
|
260
|
+
const match = /^Decision:\s*(\S+)\s*$/m.exec(text);
|
|
261
|
+
return match ? match[1] : null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function forbiddenLanguage(sectionStart, lines, plan) {
|
|
265
|
+
const findings = [];
|
|
266
|
+
lines.forEach((line, offset) => {
|
|
267
|
+
for (const [pattern, source] of FORBIDDEN) {
|
|
268
|
+
if (pattern.test(line)) {
|
|
269
|
+
findings.push(plan + ":" + (sectionStart + offset + 1) + " [ledger] open-ended coverage "
|
|
270
|
+
+ "language (" + source + ") -> enumerate every item explicitly");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return findings;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function treeMarkers(root) {
|
|
278
|
+
const result = childProcess.spawnSync("git", ["grep", "-In", "VCM:CODE", "--", ".", ":!*.md", ":!.ai"], {
|
|
279
|
+
cwd: root,
|
|
280
|
+
encoding: "utf8",
|
|
281
|
+
maxBuffer: 64 * 1024 * 1024
|
|
282
|
+
});
|
|
283
|
+
const markers = new Map();
|
|
284
|
+
const findings = [];
|
|
285
|
+
for (const raw of splitLines(result.stdout || "")) {
|
|
286
|
+
const first = raw.indexOf(":");
|
|
287
|
+
const second = first >= 0 ? raw.indexOf(":", first + 1) : -1;
|
|
288
|
+
if (first < 0 || second < 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const file = raw.slice(0, first);
|
|
292
|
+
const lineNo = Number.parseInt(raw.slice(first + 1, second), 10);
|
|
293
|
+
const content = raw.slice(second + 1);
|
|
294
|
+
for (const occurrence of content.matchAll(MARKER_ANY)) {
|
|
295
|
+
const tokenMatch = /^\s+(\S+)/.exec(content.slice(occurrence.index + occurrence[0].length));
|
|
296
|
+
if (tokenMatch === null) {
|
|
297
|
+
findings.push(file + ":" + lineNo + " [ledger] marker without an ID -> use " + B + "VCM:CODE <ID>" + B);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const markerId = tokenMatch[1];
|
|
301
|
+
if (!ID_PATTERN.test(markerId)) {
|
|
302
|
+
findings.push(file + ":" + lineNo + " [ledger] marker ID " + B + markerId + B + " must match " + ID_GRAMMAR + " exactly");
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!markers.has(markerId)) {
|
|
306
|
+
markers.set(markerId, []);
|
|
307
|
+
}
|
|
308
|
+
markers.get(markerId).push([file, lineNo]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { markers, findings };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function sortedIds(ids) {
|
|
315
|
+
return [...ids].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Mirrors str(pathlib.Path(value)): drops "." segments and repeated slashes.
|
|
319
|
+
function displayPath(value) {
|
|
320
|
+
const absolute = value.startsWith("/");
|
|
321
|
+
const parts = value.split("/").filter((part) => part !== "" && part !== ".");
|
|
322
|
+
return (absolute ? "/" : "") + parts.join("/") || ".";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isFile(target) {
|
|
326
|
+
try {
|
|
327
|
+
return fs.statSync(target).isFile();
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function main() {
|
|
334
|
+
const args = parseToolArguments(process.argv.slice(2), {
|
|
335
|
+
description: "Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.",
|
|
336
|
+
options: {
|
|
337
|
+
"--mode": { dest: "mode", required: true, choices: ["scaffold", "completion"] },
|
|
338
|
+
"--plan": { dest: "plan" },
|
|
339
|
+
"--completion": { dest: "completion" }
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const root = path.resolve(path.dirname(fs.realpathSync(__filename)), "..", "..");
|
|
344
|
+
const planPath = args.plan ? displayPath(args.plan) : path.join(root, PLAN);
|
|
345
|
+
const plan = planPath;
|
|
346
|
+
if (!isFile(planPath)) {
|
|
347
|
+
process.stdout.write("no architecture plan at " + planPath + "; nothing to check\n");
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let findings = [];
|
|
352
|
+
const manifest = section(fs.readFileSync(planPath, "utf8"), MANIFEST_HEADING);
|
|
353
|
+
if (manifest === null) {
|
|
354
|
+
process.stderr.write(planPath + ":1 [ledger] no " + B + "## Scaffold Manifest" + B + " section\n");
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
const ledger = parseLedger(manifest.start, manifest.lines, plan);
|
|
358
|
+
const entries = ledger.entries;
|
|
359
|
+
findings = findings.concat(ledger.findings, forbiddenLanguage(manifest.start, manifest.lines, plan));
|
|
360
|
+
|
|
361
|
+
const tree = treeMarkers(root);
|
|
362
|
+
const markers = tree.markers;
|
|
363
|
+
findings = findings.concat(tree.findings);
|
|
364
|
+
|
|
365
|
+
for (const entryId of sortedIds(markers.keys())) {
|
|
366
|
+
const sites = markers.get(entryId);
|
|
367
|
+
if (sites.length > 1) {
|
|
368
|
+
const where = sites.map(([file, lineNo]) => file + ":" + lineNo).join(", ");
|
|
369
|
+
findings.push("[ledger] " + entryId + " has " + sites.length + " markers (" + where + ") -> exactly one per item");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const ledgerIds = new Set(entries.keys());
|
|
374
|
+
const treeIds = new Set(markers.keys());
|
|
375
|
+
for (const entryId of sortedIds([...treeIds].filter((id) => !ledgerIds.has(id)))) {
|
|
376
|
+
const [file, lineNo] = markers.get(entryId)[0];
|
|
377
|
+
findings.push(file + ":" + lineNo + " [ledger] marker " + entryId + " has no ledger entry "
|
|
378
|
+
+ "-> every item must be manifested");
|
|
379
|
+
}
|
|
380
|
+
for (const entryId of sortedIds([...ledgerIds].filter((id) => treeIds.has(id)))) {
|
|
381
|
+
const declared = entries.get(entryId).path;
|
|
382
|
+
if (declared) {
|
|
383
|
+
for (const [file, lineNo] of markers.get(entryId)) {
|
|
384
|
+
if (!(file === declared || file.endsWith("/" + declared) || declared.endsWith("/" + file))) {
|
|
385
|
+
findings.push(file + ":" + lineNo + " [ledger] marker " + entryId + " is outside its "
|
|
386
|
+
+ "declared file " + B + declared + B);
|
|
276
387
|
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let completed = 0;
|
|
393
|
+
let failed = 0;
|
|
394
|
+
if (args.mode === "scaffold") {
|
|
395
|
+
for (const entryId of sortedIds([...ledgerIds].filter((id) => !treeIds.has(id)))) {
|
|
396
|
+
findings.push(plan + ":" + entries.get(entryId).line + " [ledger] " + entryId + " has no marker "
|
|
397
|
+
+ "in the tree -> pre-place " + B + "VCM:CODE " + entryId + B + " in its declared file");
|
|
398
|
+
}
|
|
399
|
+
} else if (!ledger.explicitEmpty) {
|
|
400
|
+
const completionPath = args.completion ? displayPath(args.completion) : path.join(root, COMPLETION);
|
|
401
|
+
if (!isFile(completionPath)) {
|
|
402
|
+
findings.push(completionPath + ":1 [completion] completion mode requires coder-completion.md");
|
|
403
|
+
} else {
|
|
404
|
+
const completionText = fs.readFileSync(completionPath, "utf8");
|
|
405
|
+
const completion = completionPath;
|
|
406
|
+
const completedSection = section(completionText, COMPLETION_HEADING);
|
|
407
|
+
let completionEntries = new Map();
|
|
408
|
+
if (completedSection === null) {
|
|
409
|
+
findings.push(completionPath + ":1 [completion] no " + B + "## Scaffold Completion" + B + " section");
|
|
410
|
+
} else {
|
|
411
|
+
const parsed = parseCompletion(completedSection.start, completedSection.lines, completion);
|
|
412
|
+
completionEntries = parsed.entries;
|
|
413
|
+
findings = findings.concat(parsed.findings);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const completionIds = new Set(completionEntries.keys());
|
|
417
|
+
for (const entryId of sortedIds([...ledgerIds].filter((id) => !completionIds.has(id)))) {
|
|
418
|
+
findings.push(plan + ":" + entries.get(entryId).line + " [completion] " + entryId + " is missing "
|
|
419
|
+
+ "from Scaffold Completion");
|
|
420
|
+
}
|
|
421
|
+
for (const entryId of sortedIds([...completionIds].filter((id) => !ledgerIds.has(id)))) {
|
|
422
|
+
findings.push(completion + ":" + completionEntries.get(entryId).line + " [completion] "
|
|
423
|
+
+ entryId + " has no Scaffold Manifest entry");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const entryId of sortedIds([...ledgerIds].filter((id) => completionIds.has(id)))) {
|
|
427
|
+
const ledgerEntry = entries.get(entryId);
|
|
428
|
+
const completionEntry = completionEntries.get(entryId);
|
|
429
|
+
const markerCount = (markers.get(entryId) || []).length;
|
|
430
|
+
const where = completion + ":" + completionEntry.line + " [completion] " + entryId;
|
|
431
|
+
if (completionEntry.action !== ledgerEntry.action) {
|
|
432
|
+
findings.push(where + " action " + B + completionEntry.action + B + " does not match Scaffold "
|
|
433
|
+
+ "Manifest action " + B + (ledgerEntry.action === null ? "None" : ledgerEntry.action) + B);
|
|
434
|
+
}
|
|
435
|
+
if (completionEntry.result === "done") {
|
|
436
|
+
completed += 1;
|
|
437
|
+
if (completionEntry.markerState !== "removed") {
|
|
438
|
+
findings.push(where + " done requires Marker State " + B + "removed" + B);
|
|
439
|
+
}
|
|
440
|
+
if (markerCount !== 0) {
|
|
441
|
+
findings.push(where + " is done but " + markerCount + " marker(s) remain -> remove the completed marker");
|
|
442
|
+
}
|
|
443
|
+
} else if (completionEntry.result === "failed") {
|
|
444
|
+
failed += 1;
|
|
445
|
+
if (completionEntry.markerState !== "present") {
|
|
446
|
+
findings.push(where + " failed requires Marker State " + B + "present" + B);
|
|
447
|
+
}
|
|
448
|
+
if (markerCount !== 1) {
|
|
449
|
+
findings.push(where + " failed requires exactly one preserved marker; found " + markerCount);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const decision = completionDecision(completionText);
|
|
455
|
+
const expectedDecision = failed ? "failed" : "ready_for_review";
|
|
456
|
+
if (decision !== expectedDecision) {
|
|
457
|
+
findings.push(completionPath + ":1 [completion] Decision must be "
|
|
458
|
+
+ B + expectedDecision + B + " for the recorded item results; found "
|
|
459
|
+
+ B + (decision || "missing") + B);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (const item of findings) {
|
|
465
|
+
process.stderr.write(item + "\n");
|
|
466
|
+
}
|
|
467
|
+
if (findings.length > 0) {
|
|
468
|
+
process.stderr.write("ledger reconciliation failed with " + findings.length + " finding(s)\n");
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
if (ledger.explicitEmpty) {
|
|
472
|
+
process.stdout.write("ledger explicitly empty: 0 ledger item(s), 0 marker(s)\n");
|
|
473
|
+
} else if (args.mode === "completion") {
|
|
474
|
+
process.stdout.write("ledger completion clean: " + ledgerIds.size + " ledger item(s), "
|
|
475
|
+
+ completed + " done, " + failed + " failed, " + treeIds.size + " marker(s) remain\n");
|
|
476
|
+
} else {
|
|
477
|
+
process.stdout.write("ledger reconciliation clean: " + ledgerIds.size + " ledger item(s), "
|
|
478
|
+
+ treeIds.size + " marker(s)\n");
|
|
479
|
+
}
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
277
482
|
|
|
278
|
-
|
|
279
|
-
findings.append(
|
|
280
|
-
f"{completion}:{section_start + 1} [completion] Scaffold Completion must "
|
|
281
|
-
f"contain the required table"
|
|
282
|
-
)
|
|
283
|
-
elif not separator_seen:
|
|
284
|
-
findings.append(
|
|
285
|
-
f"{completion}:{header_line} [completion] Scaffold Completion table is "
|
|
286
|
-
f"missing its separator row"
|
|
287
|
-
)
|
|
288
|
-
if not entries:
|
|
289
|
-
findings.append(
|
|
290
|
-
f"{completion}:{header_line or section_start + 1} [completion] Scaffold "
|
|
291
|
-
f"Completion contains no valid entries"
|
|
292
|
-
)
|
|
293
|
-
return entries, findings
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def completion_decision(completion_text: str) -> str | None:
|
|
297
|
-
match = re.search(r"^Decision:\\s*(\\S+)\\s*$", completion_text, re.MULTILINE)
|
|
298
|
-
return match.group(1) if match else None
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def forbidden_language(section_start: int, section: list[str], plan: str) -> list[str]:
|
|
302
|
-
findings = []
|
|
303
|
-
for offset, line in enumerate(section):
|
|
304
|
-
for pattern in FORBIDDEN:
|
|
305
|
-
if pattern.search(line):
|
|
306
|
-
findings.append(
|
|
307
|
-
f"{plan}:{section_start + offset + 1} [ledger] open-ended coverage "
|
|
308
|
-
f"language ({pattern.pattern}) -> enumerate every item explicitly"
|
|
309
|
-
)
|
|
310
|
-
return findings
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def tree_markers(root: Path) -> tuple[dict[str, list[tuple[str, int]]], list[str]]:
|
|
314
|
-
"""{id: [(path, line)]} for tracked source markers, plus malformed-marker findings."""
|
|
315
|
-
result = subprocess.run(
|
|
316
|
-
["git", "grep", "-In", "VCM:CODE", "--", ".", ":!*.md", ":!.ai"],
|
|
317
|
-
capture_output=True,
|
|
318
|
-
text=True,
|
|
319
|
-
cwd=root,
|
|
320
|
-
)
|
|
321
|
-
markers: dict[str, list[tuple[str, int]]] = {}
|
|
322
|
-
findings: list[str] = []
|
|
323
|
-
for raw in result.stdout.splitlines():
|
|
324
|
-
parts = raw.split(":", 2)
|
|
325
|
-
if len(parts) < 3:
|
|
326
|
-
continue
|
|
327
|
-
path, line_no, content = parts[0], int(parts[1]), parts[2]
|
|
328
|
-
for occurrence in MARKER_ANY.finditer(content):
|
|
329
|
-
token_match = re.match(r"\\s+(\\S+)", content[occurrence.end():])
|
|
330
|
-
if token_match is None:
|
|
331
|
-
findings.append(
|
|
332
|
-
f"{path}:{line_no} [ledger] marker without an ID -> "
|
|
333
|
-
f"use \`VCM:CODE <ID>\`"
|
|
334
|
-
)
|
|
335
|
-
continue
|
|
336
|
-
marker_id = token_match.group(1)
|
|
337
|
-
if not ID_PATTERN.fullmatch(marker_id):
|
|
338
|
-
findings.append(
|
|
339
|
-
f"{path}:{line_no} [ledger] marker ID \`{marker_id}\` must match "
|
|
340
|
-
f"\`[A-Z]{{2,6}}-[0-9]{{1,4}}\` exactly"
|
|
341
|
-
)
|
|
342
|
-
continue
|
|
343
|
-
markers.setdefault(marker_id, []).append((path, line_no))
|
|
344
|
-
return markers, findings
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def main() -> int:
|
|
348
|
-
parser = argparse.ArgumentParser(
|
|
349
|
-
description="Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation."
|
|
350
|
-
)
|
|
351
|
-
parser.add_argument("--mode", required=True, choices=("scaffold", "completion"))
|
|
352
|
-
parser.add_argument("--plan", default=None, help=f"plan path (default {PLAN})")
|
|
353
|
-
parser.add_argument(
|
|
354
|
-
"--completion",
|
|
355
|
-
default=None,
|
|
356
|
-
help=f"Coder completion path for completion mode (default {COMPLETION})",
|
|
357
|
-
)
|
|
358
|
-
args = parser.parse_args()
|
|
359
|
-
|
|
360
|
-
root = Path(__file__).resolve().parents[2]
|
|
361
|
-
plan_path = Path(args.plan) if args.plan else root / PLAN
|
|
362
|
-
plan = str(plan_path)
|
|
363
|
-
if not plan_path.is_file():
|
|
364
|
-
print(f"no architecture plan at {plan_path}; nothing to check")
|
|
365
|
-
return 0
|
|
366
|
-
|
|
367
|
-
findings: list[str] = []
|
|
368
|
-
section = manifest_section(plan_path.read_text(errors="replace"))
|
|
369
|
-
if section is None:
|
|
370
|
-
sys.stderr.write(f"{plan_path}:1 [ledger] no \`## Scaffold Manifest\` section\\n")
|
|
371
|
-
return 1
|
|
372
|
-
entries, parse_findings, explicit_empty = parse_ledger(*section, str(plan_path))
|
|
373
|
-
findings += parse_findings
|
|
374
|
-
findings += forbidden_language(*section, str(plan_path))
|
|
375
|
-
|
|
376
|
-
markers, marker_findings = tree_markers(root)
|
|
377
|
-
findings += marker_findings
|
|
378
|
-
|
|
379
|
-
for entry_id, sites in sorted(markers.items()):
|
|
380
|
-
if len(sites) > 1:
|
|
381
|
-
where = ", ".join(f"{p}:{n}" for p, n in sites)
|
|
382
|
-
findings.append(
|
|
383
|
-
f"[ledger] {entry_id} has {len(sites)} markers ({where}) -> exactly one per item"
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
ledger_ids = set(entries)
|
|
387
|
-
tree_ids = set(markers)
|
|
388
|
-
for entry_id in sorted(tree_ids - ledger_ids):
|
|
389
|
-
path, line_no = markers[entry_id][0]
|
|
390
|
-
findings.append(
|
|
391
|
-
f"{path}:{line_no} [ledger] marker {entry_id} has no ledger entry "
|
|
392
|
-
f"-> every item must be manifested"
|
|
393
|
-
)
|
|
394
|
-
for entry_id in sorted(ledger_ids & tree_ids):
|
|
395
|
-
entry = entries[entry_id]
|
|
396
|
-
if entry["path"]:
|
|
397
|
-
declared = entry["path"]
|
|
398
|
-
for path, line_no in markers[entry_id]:
|
|
399
|
-
if not (path == declared or path.endswith("/" + declared) or declared.endswith("/" + path)):
|
|
400
|
-
findings.append(
|
|
401
|
-
f"{path}:{line_no} [ledger] marker {entry_id} is outside its "
|
|
402
|
-
f"declared file \`{declared}\`"
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
completed = 0
|
|
406
|
-
failed = 0
|
|
407
|
-
if args.mode == "scaffold":
|
|
408
|
-
for entry_id in sorted(ledger_ids - tree_ids):
|
|
409
|
-
findings.append(
|
|
410
|
-
f"{plan}:{entries[entry_id]['line']} [ledger] {entry_id} has no marker "
|
|
411
|
-
f"in the tree -> pre-place \`VCM:CODE {entry_id}\` in its declared file"
|
|
412
|
-
)
|
|
413
|
-
elif not explicit_empty:
|
|
414
|
-
completion_path = Path(args.completion) if args.completion else root / COMPLETION
|
|
415
|
-
if not completion_path.is_file():
|
|
416
|
-
findings.append(
|
|
417
|
-
f"{completion_path}:1 [completion] completion mode requires coder-completion.md"
|
|
418
|
-
)
|
|
419
|
-
else:
|
|
420
|
-
completion_text = completion_path.read_text(errors="replace")
|
|
421
|
-
completion = str(completion_path)
|
|
422
|
-
completed_section = completion_section(completion_text)
|
|
423
|
-
if completed_section is None:
|
|
424
|
-
findings.append(
|
|
425
|
-
f"{completion_path}:1 [completion] no \`## Scaffold Completion\` section"
|
|
426
|
-
)
|
|
427
|
-
completion_entries = {}
|
|
428
|
-
else:
|
|
429
|
-
completion_entries, completion_findings = parse_completion(
|
|
430
|
-
*completed_section,
|
|
431
|
-
completion,
|
|
432
|
-
)
|
|
433
|
-
findings += completion_findings
|
|
434
|
-
|
|
435
|
-
completion_ids = set(completion_entries)
|
|
436
|
-
for entry_id in sorted(ledger_ids - completion_ids):
|
|
437
|
-
findings.append(
|
|
438
|
-
f"{plan}:{entries[entry_id]['line']} [completion] {entry_id} is missing "
|
|
439
|
-
f"from Scaffold Completion"
|
|
440
|
-
)
|
|
441
|
-
for entry_id in sorted(completion_ids - ledger_ids):
|
|
442
|
-
findings.append(
|
|
443
|
-
f"{completion}:{completion_entries[entry_id]['line']} [completion] "
|
|
444
|
-
f"{entry_id} has no Scaffold Manifest entry"
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
for entry_id in sorted(ledger_ids & completion_ids):
|
|
448
|
-
ledger_entry = entries[entry_id]
|
|
449
|
-
completion_entry = completion_entries[entry_id]
|
|
450
|
-
result = completion_entry["result"]
|
|
451
|
-
marker_state = completion_entry["marker_state"]
|
|
452
|
-
marker_count = len(markers.get(entry_id, []))
|
|
453
|
-
if completion_entry["action"] != ledger_entry["action"]:
|
|
454
|
-
findings.append(
|
|
455
|
-
f"{completion}:{completion_entry['line']} [completion] {entry_id} "
|
|
456
|
-
f"action \`{completion_entry['action']}\` does not match Scaffold "
|
|
457
|
-
f"Manifest action \`{ledger_entry['action']}\`"
|
|
458
|
-
)
|
|
459
|
-
if result == "done":
|
|
460
|
-
completed += 1
|
|
461
|
-
if marker_state != "removed":
|
|
462
|
-
findings.append(
|
|
463
|
-
f"{completion}:{completion_entry['line']} [completion] {entry_id} "
|
|
464
|
-
f"done requires Marker State \`removed\`"
|
|
465
|
-
)
|
|
466
|
-
if marker_count != 0:
|
|
467
|
-
findings.append(
|
|
468
|
-
f"{completion}:{completion_entry['line']} [completion] {entry_id} "
|
|
469
|
-
f"is done but {marker_count} marker(s) remain -> remove the completed marker"
|
|
470
|
-
)
|
|
471
|
-
elif result == "failed":
|
|
472
|
-
failed += 1
|
|
473
|
-
if marker_state != "present":
|
|
474
|
-
findings.append(
|
|
475
|
-
f"{completion}:{completion_entry['line']} [completion] {entry_id} "
|
|
476
|
-
f"failed requires Marker State \`present\`"
|
|
477
|
-
)
|
|
478
|
-
if marker_count != 1:
|
|
479
|
-
findings.append(
|
|
480
|
-
f"{completion}:{completion_entry['line']} [completion] {entry_id} "
|
|
481
|
-
f"failed requires exactly one preserved marker; found {marker_count}"
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
decision = completion_decision(completion_text)
|
|
485
|
-
expected_decision = "failed" if failed else "ready_for_review"
|
|
486
|
-
if decision != expected_decision:
|
|
487
|
-
findings.append(
|
|
488
|
-
f"{completion_path}:1 [completion] Decision must be "
|
|
489
|
-
f"\`{expected_decision}\` for the recorded item results; found "
|
|
490
|
-
f"\`{decision or 'missing'}\`"
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
for finding in findings:
|
|
494
|
-
sys.stderr.write(finding + "\\n")
|
|
495
|
-
if findings:
|
|
496
|
-
sys.stderr.write(f"ledger reconciliation failed with {len(findings)} finding(s)\\n")
|
|
497
|
-
return 1
|
|
498
|
-
if explicit_empty:
|
|
499
|
-
print("ledger explicitly empty: 0 ledger item(s), 0 marker(s)")
|
|
500
|
-
elif args.mode == "completion":
|
|
501
|
-
print(
|
|
502
|
-
f"ledger completion clean: {len(ledger_ids)} ledger item(s), "
|
|
503
|
-
f"{completed} done, {failed} failed, {len(tree_ids)} marker(s) remain"
|
|
504
|
-
)
|
|
505
|
-
else:
|
|
506
|
-
print(
|
|
507
|
-
f"ledger reconciliation clean: {len(ledger_ids)} ledger item(s), "
|
|
508
|
-
f"{len(tree_ids)} marker(s)"
|
|
509
|
-
)
|
|
510
|
-
return 0
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if __name__ == "__main__":
|
|
514
|
-
raise SystemExit(main())
|
|
483
|
+
process.exitCode = main();
|
|
515
484
|
`;
|
|
516
485
|
}
|