projectstore-claude 0.29.2 → 0.29.4
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 +1 -1
- package/bin/projectstore-claude.mjs +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
- package/node_modules/projectstore/README.md +44 -7
- package/node_modules/projectstore/commands/bar.md +30 -0
- package/node_modules/projectstore/commands/bind.md +16 -20
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/scaffold.md +24 -16
- package/node_modules/projectstore/commands/status.md +1 -1
- package/node_modules/projectstore/commands/statusline.md +2 -2
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/alongside.md +167 -0
- package/node_modules/projectstore/docs/extending.md +17 -3
- package/node_modules/projectstore/docs/getting-started.md +18 -9
- package/node_modules/projectstore/docs/harnesses.md +1 -1
- package/node_modules/projectstore/docs/how-it-works.md +12 -3
- package/node_modules/projectstore/harnesses/claude-code.json +23 -8
- package/node_modules/projectstore/harnesses/codex.json +16 -6
- package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
- package/node_modules/projectstore/hooks/band.tsx +622 -0
- package/node_modules/projectstore/hooks/hooks.json +1 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
- package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
- package/node_modules/projectstore/hooks/session-start.mjs +56 -20
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +12 -2
- package/node_modules/projectstore/scaffold/headings.json +8 -0
- package/node_modules/projectstore/scripts/band-shared.mjs +446 -0
- package/node_modules/projectstore/scripts/band.mjs +207 -0
- package/node_modules/projectstore/scripts/binding.mjs +733 -62
- package/node_modules/projectstore/scripts/build-adapters.mjs +98 -35
- package/node_modules/projectstore/scripts/cli.mjs +1463 -267
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +638 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +253 -0
- package/node_modules/projectstore/scripts/doctor.mjs +1384 -420
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +369 -0
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +283 -37
- package/node_modules/projectstore/scripts/install-harness.mjs +4701 -575
- package/node_modules/projectstore/scripts/kanban.mjs +6 -21
- package/node_modules/projectstore/scripts/lib.mjs +933 -199
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +276 -56
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +441 -72
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +139 -0
- package/node_modules/projectstore/scripts/scaffold.mjs +244 -0
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +84 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +55 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +485 -99
- package/node_modules/projectstore/scripts/term.mjs +1027 -52
- package/node_modules/projectstore/scripts/tokens.mjs +106 -28
- package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
- package/node_modules/projectstore/scripts/version-guard.mjs +79 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- package/node_modules/projectstore/templates/de/strings.json +63 -1
- package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/en/strings.json +63 -1
- package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/es/strings.json +63 -1
- package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/fr/strings.json +63 -1
- package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/ru/strings.json +63 -1
- package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/zh/strings.json +63 -1
- package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// projectstore — stages.mjs: how a story's status reads on the board.
|
|
2
|
+
//
|
|
3
|
+
// One table for two readers: the kanban board (scripts/kanban.mjs) and the
|
|
4
|
+
// band above the prompt (hooks/band.tsx), which counts an epic's stories by
|
|
5
|
+
// the same rule so the band and kanban.md never disagree. The band is a Claude
|
|
6
|
+
// Code hooks module and runs with no Node, so this file imports nothing at
|
|
7
|
+
// all — keep it that way.
|
|
8
|
+
|
|
9
|
+
// Statuses that describe work which is not on the board by definition: it was
|
|
10
|
+
// replaced, postponed, or set aside. Putting them in a column would claim the
|
|
11
|
+
// work is queued, which is the opposite of what the status says.
|
|
12
|
+
export const NOT_ACTIONABLE = new Set(["superseded", "deferred", "parked"]);
|
|
13
|
+
|
|
14
|
+
// A lower-cased status to its board column. Exact match; anything the table
|
|
15
|
+
// does not name is Backlog.
|
|
16
|
+
export function statusToColumn(status) {
|
|
17
|
+
const m = {
|
|
18
|
+
planned: "Backlog",
|
|
19
|
+
todo: "ToDo",
|
|
20
|
+
"to-do": "ToDo",
|
|
21
|
+
"in-progress": "In Progress",
|
|
22
|
+
in_progress: "In Progress",
|
|
23
|
+
"in progress": "In Progress",
|
|
24
|
+
review: "Review",
|
|
25
|
+
done: "Done",
|
|
26
|
+
closed: "Done",
|
|
27
|
+
};
|
|
28
|
+
return m[status] || "Backlog";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The band's three stages, folded from the board's columns.
|
|
32
|
+
export const COLUMN_STAGE = Object.freeze({
|
|
33
|
+
Done: "done",
|
|
34
|
+
"In Progress": "active",
|
|
35
|
+
Review: "active",
|
|
36
|
+
ToDo: "backlog",
|
|
37
|
+
Backlog: "backlog",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// A story's stage from its raw frontmatter status, as the board reads it:
|
|
41
|
+
// lower-cased; no status, or a status the board leaves off, counts nowhere
|
|
42
|
+
// (null).
|
|
43
|
+
export function stageOf(raw) {
|
|
44
|
+
if (raw === null || raw === undefined || raw === "") return null;
|
|
45
|
+
const status = String(raw).toLowerCase();
|
|
46
|
+
if (NOT_ACTIONABLE.has(status)) return null;
|
|
47
|
+
return COLUMN_STAGE[statusToColumn(status)] || "backlog";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Raw statuses → { done, active, backlog }.
|
|
51
|
+
export function foldStages(statuses) {
|
|
52
|
+
const out = { done: 0, active: 0, backlog: 0 };
|
|
53
|
+
for (const raw of statuses) {
|
|
54
|
+
const stage = stageOf(raw);
|
|
55
|
+
if (stage) out[stage]++;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Line-based frontmatter: `key: value` scalars between the opening `---` and
|
|
61
|
+
// the next one. Quoted values are JSON-decoded (titles that hold quotes),
|
|
62
|
+
// `null` is null; block sequences are invisible to it.
|
|
63
|
+
export function parseFrontmatter(md) {
|
|
64
|
+
const m = md.match(/^---\n([\s\S]*?)\n---/);
|
|
65
|
+
if (!m) return { data: {}, body: md };
|
|
66
|
+
const data = {};
|
|
67
|
+
for (const line of m[1].split("\n")) {
|
|
68
|
+
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
69
|
+
if (!kv) continue;
|
|
70
|
+
let v = kv[2].trim();
|
|
71
|
+
if (v === "null") v = null;
|
|
72
|
+
else if (v.startsWith('"') && v.endsWith('"')) {
|
|
73
|
+
// JSON.parse round-trips escaped scalars from renderTemplate's _json
|
|
74
|
+
// form (titles containing quotes); fall back to the bare strip.
|
|
75
|
+
try {
|
|
76
|
+
v = JSON.parse(v);
|
|
77
|
+
} catch {
|
|
78
|
+
v = v.slice(1, -1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
data[kv[1]] = v;
|
|
82
|
+
}
|
|
83
|
+
return { data, body: md.slice(m[0].length) };
|
|
84
|
+
}
|
|
@@ -20,7 +20,7 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
|
20
20
|
import { join, dirname } from "node:path";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { spawnSync } from "node:child_process";
|
|
23
|
-
import { pathToFileURL
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
24
|
|
|
25
25
|
// Substituted by renderStatusLineLauncher() in scripts/lib.mjs — the fallback
|
|
26
26
|
// root and the harness's own variable names (this file runs before it knows
|
|
@@ -35,7 +35,9 @@ const PLUGIN_ROOT_ENV = "__PROJECTSTORE_PLUGIN_ROOT_ENV__";
|
|
|
35
35
|
const PROJECT_DIR = "__PROJECTSTORE_PROJECT__";
|
|
36
36
|
|
|
37
37
|
function versionKey(v) {
|
|
38
|
-
const p = String(v || "0")
|
|
38
|
+
const p = String(v || "0")
|
|
39
|
+
.split(".")
|
|
40
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
39
41
|
return [p[0] || 0, p[1] || 0, p[2] || 0]; // pad: a missing component must compare as 0, not NaN
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -46,16 +48,24 @@ function versionKey(v) {
|
|
|
46
48
|
function installedRoot() {
|
|
47
49
|
try {
|
|
48
50
|
const claudeHome = (HOME_ENV && process.env[HOME_ENV]) || join(homedir(), ".claude");
|
|
49
|
-
const reg = JSON.parse(
|
|
50
|
-
readFileSync(join(claudeHome, "plugins", "installed_plugins.json"), "utf8"),
|
|
51
|
-
);
|
|
51
|
+
const reg = JSON.parse(readFileSync(join(claudeHome, "plugins", "installed_plugins.json"), "utf8"));
|
|
52
52
|
const family = dirname(FALLBACK_ROOT); // …/<marketplace>/projectstore
|
|
53
53
|
// This launcher lives at <project>/<config dir>/<runtime dir>/…: a row the
|
|
54
54
|
// host recorded for THIS checkout (local scope, one per checkout) comes
|
|
55
55
|
// first; another checkout's newer install is the fallback, not the pick.
|
|
56
56
|
let project = null;
|
|
57
|
-
try {
|
|
58
|
-
|
|
57
|
+
try {
|
|
58
|
+
project = realpathSync(PROJECT_DIR);
|
|
59
|
+
} catch {
|
|
60
|
+
project = PROJECT_DIR || null;
|
|
61
|
+
}
|
|
62
|
+
const sameProject = (p) => {
|
|
63
|
+
try {
|
|
64
|
+
return Boolean(project && p && realpathSync(p) === project);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
59
69
|
const found = [];
|
|
60
70
|
for (const [key, list] of Object.entries((reg && reg.plugins) || {})) {
|
|
61
71
|
if (key !== "projectstore" && !key.startsWith("projectstore@")) continue;
|
|
@@ -116,12 +126,20 @@ for (const root of [installedRoot(), FALLBACK_ROOT]) {
|
|
|
116
126
|
if (!rendered) {
|
|
117
127
|
let out = null;
|
|
118
128
|
try {
|
|
119
|
-
const stdin = (() => {
|
|
129
|
+
const stdin = (() => {
|
|
130
|
+
try {
|
|
131
|
+
return readFileSync(0, "utf8");
|
|
132
|
+
} catch {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
120
136
|
const projectDir = (() => {
|
|
121
137
|
try {
|
|
122
138
|
const i = JSON.parse(stdin) || {};
|
|
123
139
|
return (i.workspace && i.workspace.project_dir) || i.cwd || null;
|
|
124
|
-
} catch {
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
125
143
|
})();
|
|
126
144
|
const claudeHome = (HOME_ENV && process.env[HOME_ENV]) || join(homedir(), ".claude");
|
|
127
145
|
const candidates = [
|
|
@@ -130,11 +148,18 @@ if (!rendered) {
|
|
|
130
148
|
].filter(Boolean);
|
|
131
149
|
for (const p of candidates) {
|
|
132
150
|
let cmd = null;
|
|
133
|
-
try {
|
|
151
|
+
try {
|
|
152
|
+
cmd = JSON.parse(readFileSync(p, "utf8"))?.statusLine?.command;
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
134
156
|
if (typeof cmd !== "string" || !cmd.trim()) continue;
|
|
135
157
|
if (cmd.includes("statusline.mjs")) continue; // ours — would recurse
|
|
136
158
|
const r = spawnSync(cmd, { shell: true, input: stdin, encoding: "utf8", timeout: 2000, maxBuffer: 1 << 20 });
|
|
137
|
-
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
159
|
+
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
160
|
+
out = r.stdout.replace(/\s+$/, "");
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
138
163
|
}
|
|
139
164
|
} catch {}
|
|
140
165
|
process.stdout.write((out || "") + "\n");
|
|
@@ -18,13 +18,21 @@
|
|
|
18
18
|
// vault reads (ADR-006):
|
|
19
19
|
// 1. this session's pointer file
|
|
20
20
|
// (<project>/.projectstore/state/sessions/<session_id>.json — written by
|
|
21
|
-
// the
|
|
21
|
+
// the tool hooks with denormalized titles, a write's after the write;
|
|
22
|
+
// by `session refresh` after a story close; and at SessionStart), else
|
|
22
23
|
// 2. an explicit localized cold-start line ("no epic or story in this
|
|
23
24
|
// session yet"). Never blank while enabled; a pointer that exists but
|
|
24
25
|
// fails to parse renders an error-marked string, never the cheerful
|
|
25
26
|
// cold-start line. The renderer drops a .last-render.json breadcrumb so
|
|
26
27
|
// doctor can detect session_id divergence between hook and statusLine.
|
|
27
28
|
//
|
|
29
|
+
// While this session's band above the prompt is drawing (hooks/band.tsx keeps
|
|
30
|
+
// a heartbeat fresh in the session-state directory), our line gives way to it:
|
|
31
|
+
// composed, the base HUD prints alone; standalone, the badge and the 📚
|
|
32
|
+
// segment drop. A missing, unreadable or stale heartbeat changes nothing, so
|
|
33
|
+
// a band that cannot draw always gives the line back (the band ADR, decision
|
|
34
|
+
// 5; spec contract 11).
|
|
35
|
+
//
|
|
28
36
|
// Everything is best-effort; on any error emit what we have and exit 0.
|
|
29
37
|
|
|
30
38
|
import { readFileSync, existsSync } from "node:fs";
|
|
@@ -41,6 +49,7 @@ import {
|
|
|
41
49
|
claudeHome,
|
|
42
50
|
writeFileAtomic,
|
|
43
51
|
} from "./lib.mjs";
|
|
52
|
+
import { heartbeatFile, heartbeatYields, storyIsCurrent } from "./band-shared.mjs";
|
|
44
53
|
|
|
45
54
|
const SELF = fileURLToPath(import.meta.url);
|
|
46
55
|
const SEP = " · ";
|
|
@@ -53,7 +62,11 @@ const ARROW = " › ";
|
|
|
53
62
|
process.stdout.on("error", () => process.exit(0));
|
|
54
63
|
|
|
55
64
|
function readRawStdin() {
|
|
56
|
-
try {
|
|
65
|
+
try {
|
|
66
|
+
return readFileSync(0, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
const FALLBACK_STRINGS = {
|
|
@@ -146,13 +159,29 @@ function bookSegment(cfg, proj, input, strings) {
|
|
|
146
159
|
if (!state || !state.active_epic) return BOOK + strings.statusline_no_work;
|
|
147
160
|
|
|
148
161
|
let seg = BOOK + (state.epic_title || state.active_epic);
|
|
149
|
-
|
|
162
|
+
// The story only while it is work the session is moving — the band's rule
|
|
163
|
+
// (ADR-006, amended 2026-10-12): a done, closed, parked or status-less
|
|
164
|
+
// story leaves the epic alone.
|
|
165
|
+
if (state.active_story && storyIsCurrent(state.story_status)) {
|
|
150
166
|
seg += ARROW + (state.story_title || state.active_story);
|
|
151
167
|
if (state.story_status) seg += ` (${state.story_status})`;
|
|
152
168
|
}
|
|
153
169
|
return seg;
|
|
154
170
|
}
|
|
155
171
|
|
|
172
|
+
// Whether this session's band is drawing now. Read after bookSegment has
|
|
173
|
+
// written the breadcrumb, so doctor keeps its evidence while the band is up;
|
|
174
|
+
// any failure reads as "no band" and the line stays.
|
|
175
|
+
function bandYields(proj, sid) {
|
|
176
|
+
try {
|
|
177
|
+
const name = heartbeatFile(sid);
|
|
178
|
+
if (!name) return false;
|
|
179
|
+
return heartbeatYields(JSON.parse(readFileSync(join(stateDir(proj), name), "utf8")), Date.now());
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
156
185
|
// The base statusLine command we compose with — highest-precedence entry BELOW
|
|
157
186
|
// our own local one: project settings.json, else user settings.json.
|
|
158
187
|
function discoverBaseCommand(projectDir) {
|
|
@@ -193,25 +222,38 @@ function runBase(cmd, rawStdin, env) {
|
|
|
193
222
|
|
|
194
223
|
function main() {
|
|
195
224
|
const raw = readRawStdin();
|
|
196
|
-
let input
|
|
197
|
-
try {
|
|
225
|
+
let input;
|
|
226
|
+
try {
|
|
227
|
+
input = JSON.parse(raw) || {};
|
|
228
|
+
} catch {
|
|
229
|
+
input = {};
|
|
230
|
+
}
|
|
198
231
|
|
|
199
232
|
// The status line gets no project variable from the harness; the project is
|
|
200
233
|
// on stdin, and it deliberately beats any exported variable. Read the config
|
|
201
234
|
// for that directory directly — never by mutating process.env, which the
|
|
202
235
|
// composed base command would then inherit.
|
|
203
|
-
const projectDir =
|
|
204
|
-
(input.workspace && input.workspace.project_dir) || input.cwd || process.cwd();
|
|
236
|
+
const projectDir = (input.workspace && input.workspace.project_dir) || input.cwd || process.cwd();
|
|
205
237
|
|
|
206
|
-
let cfg
|
|
207
|
-
try {
|
|
238
|
+
let cfg;
|
|
239
|
+
try {
|
|
240
|
+
cfg = readConfigAt(projectDir);
|
|
241
|
+
} catch {
|
|
242
|
+
cfg = null;
|
|
243
|
+
}
|
|
208
244
|
|
|
209
245
|
const strings = loadStrings(cfg && cfg.language);
|
|
210
|
-
let book
|
|
211
|
-
try {
|
|
246
|
+
let book;
|
|
247
|
+
try {
|
|
248
|
+
book = cfg && cfg.vault_path ? bookSegment(cfg, projectDir, input, strings) : null;
|
|
249
|
+
} catch {
|
|
250
|
+
book = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const yielded = Boolean(cfg && cfg.vault_path && bandYields(projectDir, input.session_id));
|
|
212
254
|
|
|
213
255
|
const showVersion = !(cfg && cfg.statusline && cfg.statusline.show_version === false);
|
|
214
|
-
const ver = showVersion ? pluginVersion() : null;
|
|
256
|
+
const ver = showVersion && !yielded ? pluginVersion() : null;
|
|
215
257
|
const badge = ver ? `[PS#${ver}] ` : "";
|
|
216
258
|
|
|
217
259
|
const baseCmd = discoverBaseCommand(projectDir);
|
|
@@ -219,8 +261,7 @@ function main() {
|
|
|
219
261
|
|
|
220
262
|
// "Never blank" must be total: even if the resolver threw (book === null on a
|
|
221
263
|
// bound project), fall back to the cold-start line rather than vanishing.
|
|
222
|
-
const bookOut =
|
|
223
|
-
cfg && cfg.vault_path ? book || BOOK + strings.statusline_no_work : null;
|
|
264
|
+
const bookOut = cfg && cfg.vault_path && !yielded ? book || BOOK + strings.statusline_no_work : null;
|
|
224
265
|
|
|
225
266
|
const lines = [];
|
|
226
267
|
if (baseOut) {
|
|
@@ -132,9 +132,13 @@ function main() {
|
|
|
132
132
|
const status = String(fm.status || "").toLowerCase();
|
|
133
133
|
|
|
134
134
|
if (mode === "plan") {
|
|
135
|
-
const r = insertSection(
|
|
135
|
+
const r = insertSection(
|
|
136
|
+
text,
|
|
137
|
+
"implementation_plan",
|
|
138
|
+
lang,
|
|
136
139
|
"<!-- Route through the covering spec's contracts: which contracts, in what order, which files. Filled at the gate. -->",
|
|
137
|
-
["decomposition", "description"]
|
|
140
|
+
["decomposition", "description"],
|
|
141
|
+
);
|
|
138
142
|
text = r.text;
|
|
139
143
|
if (r.inserted) notes.push("inserted Implementation Plan section");
|
|
140
144
|
if (!["in-progress", "in_progress", "review", "done"].includes(status)) {
|
|
@@ -148,9 +152,13 @@ function main() {
|
|
|
148
152
|
text = setFm(text, "plan_updated_at", JSON.stringify(now));
|
|
149
153
|
notes.push("stamped plan_updated_at");
|
|
150
154
|
} else {
|
|
151
|
-
const r = insertSection(
|
|
155
|
+
const r = insertSection(
|
|
156
|
+
text,
|
|
157
|
+
"final_summary",
|
|
158
|
+
lang,
|
|
152
159
|
"<!-- What changed, why, tests executed, risks and follow-ups. Filled at the gate. -->",
|
|
153
|
-
["acceptance", "decomposition", "description"]
|
|
160
|
+
["acceptance", "decomposition", "description"],
|
|
161
|
+
);
|
|
154
162
|
text = r.text;
|
|
155
163
|
if (r.inserted) notes.push("inserted Final Summary section");
|
|
156
164
|
if (status !== "done") {
|
|
@@ -181,28 +189,42 @@ function main() {
|
|
|
181
189
|
// otherwise shifts every later pairing and the report drowns in
|
|
182
190
|
// artifacts of the shift instead of naming the change. Line numbers are
|
|
183
191
|
// post-normalization (volatile keys may have been inserted).
|
|
184
|
-
const al = a.split("\n"),
|
|
192
|
+
const al = a.split("\n"),
|
|
193
|
+
bl = b.split("\n");
|
|
185
194
|
let lo = 0;
|
|
186
195
|
while (lo < al.length && lo < bl.length && al[lo] === bl[lo]) lo++;
|
|
187
|
-
let hiA = al.length - 1,
|
|
188
|
-
|
|
196
|
+
let hiA = al.length - 1,
|
|
197
|
+
hiB = bl.length - 1;
|
|
198
|
+
while (hiA > lo && hiB > lo && al[hiA] === bl[hiB]) {
|
|
199
|
+
hiA--;
|
|
200
|
+
hiB--;
|
|
201
|
+
}
|
|
189
202
|
for (let i = 0; i <= Math.max(hiA, hiB) - lo && drift.length < 5; i++) {
|
|
190
|
-
const x = al[lo + i],
|
|
203
|
+
const x = al[lo + i],
|
|
204
|
+
y = bl[lo + i];
|
|
191
205
|
if (lo + i > hiA && lo + i > hiB) break;
|
|
192
|
-
drift.push(
|
|
206
|
+
drift.push(
|
|
207
|
+
`normalized line ${lo + i + 1}: ${JSON.stringify((x ?? "«absent»").slice(0, 120))} vs ${JSON.stringify((y ?? "«absent»").slice(0, 120))}`,
|
|
208
|
+
);
|
|
193
209
|
}
|
|
194
210
|
}
|
|
195
211
|
check = { baseline: resolve(checkPath), match: a === b, drift };
|
|
196
212
|
}
|
|
197
213
|
|
|
198
|
-
process.stdout.write(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
process.stdout.write(
|
|
215
|
+
JSON.stringify(
|
|
216
|
+
{
|
|
217
|
+
path: abs,
|
|
218
|
+
mode,
|
|
219
|
+
changed: text !== original,
|
|
220
|
+
check,
|
|
221
|
+
notes,
|
|
222
|
+
content: text,
|
|
223
|
+
},
|
|
224
|
+
null,
|
|
225
|
+
2,
|
|
226
|
+
) + "\n",
|
|
227
|
+
);
|
|
206
228
|
}
|
|
207
229
|
|
|
208
230
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|