pi-gauntlet 5.18.4 → 5.18.6
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
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v5.18.6 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `gauntlet-spec-index` prints the recorded modified-file paths that still exist in the repository in its `files` column, `;`-separated, with `missing` marking a telemetry record that has no list; the brainstorming scout renders them as a `Predecessor anchors` section for named predecessors. (#52)
|
|
8
|
+
|
|
9
|
+
## v5.18.5 - 2026-09-23
|
|
10
|
+
|
|
11
|
+
- The TDD RED step directs authors to reuse existing setup helpers and consolidate near-identical cases into parametrized or table-driven examples. (#49)
|
|
12
|
+
|
|
3
13
|
## v5.18.4 - 2026-09-23
|
|
4
14
|
|
|
5
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ Pin an exact release with `npm:pi-gauntlet@X.Y.Z`. See [doc/install-internals.md
|
|
|
134
134
|
|
|
135
135
|
## Spec search index
|
|
136
136
|
|
|
137
|
-
`gauntlet-spec-index` provides lexical search across `doc/specs/*.md` at the repository root and one service level down. From a repository worktree, run `node <pi-gauntlet-package>/bin/gauntlet-spec-index.mjs --query "<text>" [--limit N]`; it requires Node >=24.15.0, refreshes its FTS5 index on every query, and prints tab-separated `score`, `path`, `service`, `title`, `status`, `shipped_at`, `files`, and `snippet` columns. The per-worktree cache lives at `.pi/gauntlet/index.sqlite`, and its first creation adds `/.pi/gauntlet/index.sqlite*` to Git's `info/exclude` so the database and SQLite sidecars stay out of `git status`.
|
|
137
|
+
`gauntlet-spec-index` provides lexical search across `doc/specs/*.md` at the repository root and one service level down. From a repository worktree, run `node <pi-gauntlet-package>/bin/gauntlet-spec-index.mjs --query "<text>" [--limit N]`; it requires Node >=24.15.0, refreshes its FTS5 index on every query, and prints tab-separated `score`, `path`, `service`, `title`, `status`, `shipped_at`, `files`, and `snippet` columns. The `files` column is a `;`-separated list of repo-relative paths the spec's shipped change modified and that still exist in the repository, the literal `missing` when the spec's telemetry record has no `derived.modified_files` list, or blank when there is no readable record or no recorded path remains. The per-worktree cache lives at `.pi/gauntlet/index.sqlite`, and its first creation adds `/.pi/gauntlet/index.sqlite*` to Git's `info/exclude` so the database and SQLite sidecars stay out of `git status`.
|
|
138
138
|
|
|
139
139
|
## Performance digest
|
|
140
140
|
|
|
@@ -176,7 +176,7 @@ const toMatch = (query) =>
|
|
|
176
176
|
query.split(/\s+/).filter((t) => t.length >= 2).map((t) => `"${t.replaceAll('"', '""')}"`).join(" OR ");
|
|
177
177
|
|
|
178
178
|
function telemetry(root, specPath) {
|
|
179
|
-
const blank = { status: null, shipped_at: null, files:
|
|
179
|
+
const blank = { status: null, shipped_at: null, files: "" };
|
|
180
180
|
const p = join(root, ".pi/gauntlet/telemetry", specPath.replace(/\.md$/, ".yaml"));
|
|
181
181
|
if (!existsSync(p)) return blank;
|
|
182
182
|
let rec;
|
|
@@ -186,12 +186,17 @@ function telemetry(root, specPath) {
|
|
|
186
186
|
process.stderr.write(`gauntlet-spec-index: warning: unreadable telemetry ${p}\n`);
|
|
187
187
|
return blank;
|
|
188
188
|
}
|
|
189
|
-
if (!rec || typeof rec !== "object") return blank;
|
|
189
|
+
if (!rec || typeof rec !== "object" || Array.isArray(rec)) return blank;
|
|
190
190
|
const mf = rec.derived?.modified_files;
|
|
191
|
-
|
|
191
|
+
const files = Array.isArray(mf)
|
|
192
|
+
? mf.filter((f) => typeof f === "string" && existsSync(join(root, f))).join(";")
|
|
193
|
+
: "missing";
|
|
194
|
+
return { status: rec.status ?? null, shipped_at: rec.shipped_at ?? null, files };
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
const cell = (v) => (v === null || v === undefined ? "" : String(v).replace(/\s+/g, " ").trim());
|
|
198
|
+
// Paths keep their spaces; only column and row delimiters are neutralised.
|
|
199
|
+
const filesCell = (v) => v.replace(/[\t\r\n]+/g, " ");
|
|
195
200
|
|
|
196
201
|
async function main() {
|
|
197
202
|
if (!nodeOk()) die(`gauntlet-spec-index needs Node >=24.15.0 (found ${process.versions.node})`);
|
|
@@ -211,7 +216,7 @@ async function main() {
|
|
|
211
216
|
const out = [HEADER.join("\t")];
|
|
212
217
|
for (const r of rows) {
|
|
213
218
|
const t = telemetry(root, r.path);
|
|
214
|
-
out.push([r.score.toFixed(3), r.path, r.service, r.title, t.status, t.shipped_at, t.files, r.snippet].
|
|
219
|
+
out.push([...[r.score.toFixed(3), r.path, r.service, r.title, t.status, t.shipped_at].map(cell), filesCell(t.files), cell(r.snippet)].join("\t"));
|
|
215
220
|
}
|
|
216
221
|
process.stdout.write(out.join("\n") + "\n");
|
|
217
222
|
db.close();
|
|
@@ -112,13 +112,17 @@ test("6: telemetry join is output-only and tolerant", (t) => {
|
|
|
112
112
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
113
113
|
run(root, ["--query", "zephyr"]);
|
|
114
114
|
const filesBefore = withDb(root, (database) => JSON.stringify(database.prepare("SELECT * FROM files ORDER BY path").all()));
|
|
115
|
+
write(root, "x", "x\n");
|
|
116
|
+
write(root, "y", "y\n");
|
|
115
117
|
write(root, ".pi/gauntlet/telemetry/doc/specs/a.yaml", "status: shipped\nshipped_at: 2026-09-17T10:00:00Z\nderived:\n modified_files:\n - x\n - y\n");
|
|
116
118
|
let r = run(root, ["--query", "zephyr"]);
|
|
117
|
-
assert.deepEqual(r.rows[0].slice(4, 7), ["shipped", "2026-09-17T10:00:00Z", "
|
|
119
|
+
assert.deepEqual(r.rows[0].slice(4, 7), ["shipped", "2026-09-17T10:00:00Z", "x;y"]);
|
|
120
|
+
assert.equal(r.rows[0].length, 8);
|
|
121
|
+
assert.equal(r.rows[1][6], "");
|
|
118
122
|
assert.equal(withDb(root, (database) => JSON.stringify(database.prepare("SELECT * FROM files ORDER BY path").all())), filesBefore);
|
|
119
123
|
write(root, ".pi/gauntlet/telemetry/doc/specs/a.yaml", "status: in_progress\nshipped_at: 2026-09-18T10:00:00Z\n");
|
|
120
124
|
r = run(root, ["--query", "zephyr"]);
|
|
121
|
-
assert.deepEqual(r.rows[0].slice(4, 7), ["in_progress", "2026-09-18T10:00:00Z", ""]);
|
|
125
|
+
assert.deepEqual(r.rows[0].slice(4, 7), ["in_progress", "2026-09-18T10:00:00Z", "missing"]);
|
|
122
126
|
write(root, ".pi/gauntlet/telemetry/doc/specs/a.yaml", "status: [unclosed\n");
|
|
123
127
|
r = run(root, ["--query", "zephyr"]);
|
|
124
128
|
assert.equal(r.status, 0);
|
|
@@ -126,6 +130,51 @@ test("6: telemetry join is output-only and tolerant", (t) => {
|
|
|
126
130
|
assert.match(r.stderr, /a\.yaml/);
|
|
127
131
|
});
|
|
128
132
|
|
|
133
|
+
test("6b: files cell is the AC fixture - present paths kept, gone paths dropped, no list is missing", (t) => {
|
|
134
|
+
const root = mkdtempSync(join(tmpdir(), "gsi-"));
|
|
135
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
136
|
+
spawnSync("git", ["init", "-q"], { cwd: root });
|
|
137
|
+
write(root, "doc/specs/a.md", "# Alpha quokka\n\n**Goal:** quokka.\n");
|
|
138
|
+
write(root, "doc/specs/b.md", "# Beta quokka\n\n**Goal:** quokka too.\n");
|
|
139
|
+
write(root, "src/x.ts", "export {};\n");
|
|
140
|
+
write(root, ".pi/gauntlet/telemetry/doc/specs/a.yaml", "status: shipped\nderived:\n modified_files:\n - src/x.ts\n - src/gone.ts\n");
|
|
141
|
+
write(root, ".pi/gauntlet/telemetry/doc/specs/b.yaml", "status: shipped\n");
|
|
142
|
+
spawnSync("git", ["add", "-A"], { cwd: root });
|
|
143
|
+
spawnSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", "fixture"], { cwd: root });
|
|
144
|
+
const r = run(root, ["--query", "quokka"]);
|
|
145
|
+
assert.equal(r.status, 0, r.stderr);
|
|
146
|
+
const cells = Object.fromEntries(r.rows.map((row) => [row[1], row[6]]));
|
|
147
|
+
assert.equal(cells["doc/specs/a.md"], "src/x.ts");
|
|
148
|
+
assert.equal(cells["doc/specs/b.md"], "missing");
|
|
149
|
+
for (const row of r.rows) assert.equal(row.length, 8);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("6c: files cell edge cases - order, spaces kept, all gone, non-array, null record", (t) => {
|
|
153
|
+
const root = repo();
|
|
154
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
155
|
+
const yaml = ".pi/gauntlet/telemetry/doc/specs/a.yaml";
|
|
156
|
+
const files = () => run(root, ["--query", "zephyr"]).rows[0][6];
|
|
157
|
+
write(root, "b.js", "");
|
|
158
|
+
write(root, "a.js", "");
|
|
159
|
+
write(root, "sp aced.txt", "");
|
|
160
|
+
write(root, yaml, "status: shipped\nderived:\n modified_files:\n - a.js\n - b.js\n");
|
|
161
|
+
assert.equal(files(), "a.js;b.js");
|
|
162
|
+
write(root, "tab\there.js", "");
|
|
163
|
+
write(root, yaml, 'status: shipped\nderived:\n modified_files:\n - a.js\n - "tab\\there.js"\n');
|
|
164
|
+
assert.equal(files(), "a.js;tab here.js");
|
|
165
|
+
assert.equal(run(root, ["--query", "zephyr"]).rows[0].length, 8);
|
|
166
|
+
write(root, yaml, "status: shipped\nderived:\n modified_files:\n - 'sp aced.txt'\n - 7\n");
|
|
167
|
+
assert.equal(files(), "sp aced.txt");
|
|
168
|
+
write(root, yaml, "status: shipped\nderived:\n modified_files:\n - nope1\n - nope2\n");
|
|
169
|
+
assert.equal(files(), "");
|
|
170
|
+
write(root, yaml, 'status: shipped\nderived:\n modified_files: "x"\n');
|
|
171
|
+
assert.equal(files(), "missing");
|
|
172
|
+
write(root, yaml, "");
|
|
173
|
+
assert.equal(files(), "");
|
|
174
|
+
write(root, yaml, "- a\n- b\n");
|
|
175
|
+
assert.equal(files(), "");
|
|
176
|
+
});
|
|
177
|
+
|
|
129
178
|
test("7: query sanitising tolerates embedded quotes", (t) => {
|
|
130
179
|
const root = repo();
|
|
131
180
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
package/package.json
CHANGED
|
@@ -63,8 +63,23 @@ Scout (always dispatched):
|
|
|
63
63
|
> handoff. Either way open at
|
|
64
64
|
> most five candidates whose topic matches this request, and name any whose design
|
|
65
65
|
> this request replaces or amends with the section(s) affected - `Predecessor:
|
|
66
|
-
> <path>, <scope>` - or `Predecessor: none`.
|
|
67
|
-
>
|
|
66
|
+
> <path>, <scope>` - or `Predecessor: none`.
|
|
67
|
+
> Judge by topic; shared file paths never decide.
|
|
68
|
+
> The `files` column of each candidate row is the `;`-separated list of repo-relative
|
|
69
|
+
> paths that predecessor's ship modified and that still exist, or the literal `missing`,
|
|
70
|
+
> or blank; do not recompute it from git or telemetry. After the `Predecessor:` line(s),
|
|
71
|
+
> and only when at least one predecessor is named, render a `Predecessor anchors`
|
|
72
|
+
> section: list every attributed path exactly once, attributed to the first named
|
|
73
|
+
> predecessor in index output order whose cell lists it, with no per-path commentary
|
|
74
|
+
> (bookkeeping paths such as `CHANGELOG.md` are listed like any other); for each named
|
|
75
|
+
> predecessor whose cell is `missing`, write one line
|
|
76
|
+
> `<spec>: modified file list missing for this spec`, where `<spec>` is the path exactly
|
|
77
|
+
> as written in its `Predecessor:` line. A blank cell contributes nothing for that spec;
|
|
78
|
+
> a named predecessor with no index row (reached through a supersession banner, or named
|
|
79
|
+
> from the directory-listing fallback) contributes no path and no `missing` line. When
|
|
80
|
+
> no path and no `missing` line results - including whenever the index was unavailable -
|
|
81
|
+
> omit the section entirely; `Predecessor: none` produces no anchors section. The anchors
|
|
82
|
+
> are a recon hint, never a selection input. End with an
|
|
68
83
|
> "Open questions that matter for the spec"
|
|
69
84
|
> section. Compact handoff, not a dump.
|
|
70
85
|
|
|
@@ -60,6 +60,8 @@ Implement fresh from tests. Period.
|
|
|
60
60
|
|
|
61
61
|
Write one minimal test showing what should happen.
|
|
62
62
|
|
|
63
|
+
Before writing setup, look for an existing helper, fixture, or factory. Extend a suitable existing one before creating another. Write near-identical cases for the same behavior as one parametrized or table-driven example.
|
|
64
|
+
|
|
63
65
|
**Requirements:**
|
|
64
66
|
- One behavior per test
|
|
65
67
|
- Clear name describing behavior (if the name contains "and", split it)
|