@tekyzinc/gsd-t 3.29.10 → 3.29.11
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 +30 -0
- package/bin/gsd-t-test-data-adapters/file-json-array.cjs +27 -3
- package/bin/gsd-t-test-data-adapters/localstorage-key-prefix.cjs +6 -1
- package/bin/gsd-t-test-data-adapters/sqlite-table-where.cjs +16 -1
- package/bin/gsd-t-test-data-ledger.cjs +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.29.11] - 2026-05-29 09:57 PDT
|
|
6
|
+
|
|
7
|
+
### Fixed — CRITICAL test-data adapter data-destruction bug (M60)
|
|
8
|
+
|
|
9
|
+
Found by a native-Workflow Red Team bake-off (Opus 4.8, perspective-diverse
|
|
10
|
+
adversarial review) while evaluating GSD-T against native Claude Code 4.8
|
|
11
|
+
capability. M58's own in-session Red Team had passed this as "6/6 defended"
|
|
12
|
+
— the claim was wrong.
|
|
13
|
+
|
|
14
|
+
- **Bug**: `bin/gsd-t-test-data-adapters/file-json-array.cjs` and
|
|
15
|
+
`localstorage-key-prefix.cjs` guarded with
|
|
16
|
+
`typeof taggedPrefix === 'string' && taggedPrefix.length > 0 && !id.startsWith(...)`.
|
|
17
|
+
An empty/undefined `taggedPrefix` short-circuits the whole condition, so
|
|
18
|
+
**no guard runs** — `gsd-t test-data --purge` would delete untagged
|
|
19
|
+
production records and report `errors:0` (gate GREEN). No adapter enforced
|
|
20
|
+
`projectDir` containment on the `store` path, making it a write-anywhere
|
|
21
|
+
delete primitive. Reproduced live before fixing.
|
|
22
|
+
- **Fix** (additive refusal only — removes capability, never adds destructive
|
|
23
|
+
behavior): both adapters now hard-refuse empty/undefined `taggedPrefix`
|
|
24
|
+
(matching the already-correct `sqlite-table-where` adapter); `file-json-array`
|
|
25
|
+
and `sqlite-table-where` enforce the containment predicate
|
|
26
|
+
`resolved.startsWith(root + sep) && resolved !== root` when `projectDir` is
|
|
27
|
+
supplied; `purgeRunInserts` threads `projectDir` into every adapter call.
|
|
28
|
+
- **Tests**: new `test/m60-redteam-regressions.test.js` (10 tests, one per
|
|
29
|
+
finding + happy-path + back-compat). M58 suite 44/44 unchanged.
|
|
30
|
+
- **Contract**: `test-data-tagging-contract.md` → v1.1.0 STABLE (empty-prefix
|
|
31
|
+
refusal + path containment now normative).
|
|
32
|
+
- Backward-compatible: the happy path (non-empty prefix, in-project path, or
|
|
33
|
+
no `projectDir`) is unchanged.
|
|
34
|
+
|
|
5
35
|
## [3.29.10] - 2026-05-27 10:09 PDT
|
|
6
36
|
|
|
7
37
|
### Changed — Timestamp precision in progress.md (forward-only)
|
|
@@ -5,23 +5,47 @@
|
|
|
5
5
|
* `store` is the file path; `id` is the value of the `id` field on the matching row.
|
|
6
6
|
*
|
|
7
7
|
* Refuses to delete a record whose `id` does not start with `taggedPrefix`.
|
|
8
|
-
*
|
|
8
|
+
* `taggedPrefix` is REQUIRED and non-empty — an empty/omitted prefix would
|
|
9
|
+
* disable the guard entirely (Red Team CRITICAL, M60). Atomic rewrite
|
|
10
|
+
* (write-temp + rename).
|
|
11
|
+
*
|
|
12
|
+
* When `projectDir` is supplied, `store` MUST resolve inside it — the adapter
|
|
13
|
+
* refuses paths outside-AND-equal-to projectDir (containment predicate from
|
|
14
|
+
* feedback_destructive_path_ops_containment). Prevents a tampered ledger from
|
|
15
|
+
* becoming a write-anywhere delete primitive.
|
|
9
16
|
*/
|
|
10
17
|
const fs = require('node:fs');
|
|
11
18
|
const path = require('node:path');
|
|
12
19
|
|
|
13
20
|
const KIND = 'file-json-array';
|
|
14
21
|
|
|
15
|
-
function
|
|
22
|
+
function assertContained(store, projectDir) {
|
|
23
|
+
if (typeof projectDir !== 'string' || projectDir.length === 0) {
|
|
24
|
+
return; // no projectDir supplied — containment not enforceable, caller's choice
|
|
25
|
+
}
|
|
26
|
+
const root = path.resolve(projectDir);
|
|
27
|
+
const resolved = path.resolve(store);
|
|
28
|
+
if (!(resolved.startsWith(root + path.sep) && resolved !== root)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`file-json-array: store path "${store}" resolves outside projectDir "${projectDir}" — refused (containment guard)`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function purge({ store, id, taggedPrefix, projectDir }) {
|
|
16
36
|
if (typeof store !== 'string' || store.length === 0) {
|
|
17
37
|
throw new Error('file-json-array: store must be a non-empty file path');
|
|
18
38
|
}
|
|
19
39
|
if (typeof id !== 'string' || id.length === 0) {
|
|
20
40
|
throw new Error('file-json-array: id must be a non-empty string');
|
|
21
41
|
}
|
|
22
|
-
if (typeof taggedPrefix
|
|
42
|
+
if (typeof taggedPrefix !== 'string' || taggedPrefix.length === 0) {
|
|
43
|
+
throw new Error('file-json-array: taggedPrefix is required and must be non-empty (guard cannot be disabled)');
|
|
44
|
+
}
|
|
45
|
+
if (!id.startsWith(taggedPrefix)) {
|
|
23
46
|
throw new Error(`file-json-array: tag prefix mismatch (id="${id}", taggedPrefix="${taggedPrefix}")`);
|
|
24
47
|
}
|
|
48
|
+
assertContained(store, projectDir);
|
|
25
49
|
|
|
26
50
|
if (!fs.existsSync(store)) {
|
|
27
51
|
return 'absent';
|
|
@@ -22,7 +22,12 @@ async function purge({ page, store, id, taggedPrefix }) {
|
|
|
22
22
|
if (typeof id !== 'string' || id.length === 0) {
|
|
23
23
|
throw new Error('localStorage-key-prefix: id must be a non-empty string');
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// taggedPrefix is REQUIRED and non-empty — an empty/omitted prefix would
|
|
26
|
+
// disable the guard entirely (Red Team CRITICAL, M60).
|
|
27
|
+
if (typeof taggedPrefix !== 'string' || taggedPrefix.length === 0) {
|
|
28
|
+
throw new Error('localStorage-key-prefix: taggedPrefix is required and must be non-empty (guard cannot be disabled)');
|
|
29
|
+
}
|
|
30
|
+
if (!id.startsWith(taggedPrefix)) {
|
|
26
31
|
throw new Error(`localStorage-key-prefix: tag prefix mismatch (id="${id}", taggedPrefix="${taggedPrefix}")`);
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -8,11 +8,25 @@
|
|
|
8
8
|
* still loads when the module isn't installed. Tests self-skip in that case.
|
|
9
9
|
*/
|
|
10
10
|
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
11
12
|
|
|
12
13
|
const KIND = 'sqlite-table-where';
|
|
13
14
|
|
|
14
15
|
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
16
|
|
|
17
|
+
function assertContained(dbPath, projectDir) {
|
|
18
|
+
if (typeof projectDir !== 'string' || projectDir.length === 0) {
|
|
19
|
+
return; // no projectDir supplied — containment not enforceable, caller's choice
|
|
20
|
+
}
|
|
21
|
+
const root = path.resolve(projectDir);
|
|
22
|
+
const resolved = path.resolve(dbPath);
|
|
23
|
+
if (!(resolved.startsWith(root + path.sep) && resolved !== root)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`sqlite-table-where: dbPath "${dbPath}" resolves outside projectDir "${projectDir}" — refused (containment guard)`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
function parseStore(store) {
|
|
17
31
|
if (typeof store !== 'string') {
|
|
18
32
|
throw new Error('sqlite-table-where: store must be "dbPath|table|idColumn"');
|
|
@@ -34,7 +48,7 @@ function parseStore(store) {
|
|
|
34
48
|
return { dbPath, table, idColumn };
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
function purge({ store, id, taggedPrefix }) {
|
|
51
|
+
function purge({ store, id, taggedPrefix, projectDir }) {
|
|
38
52
|
const { dbPath, table, idColumn } = parseStore(store);
|
|
39
53
|
if (typeof id !== 'string' || id.length === 0) {
|
|
40
54
|
throw new Error('sqlite-table-where: id must be a non-empty string');
|
|
@@ -45,6 +59,7 @@ function purge({ store, id, taggedPrefix }) {
|
|
|
45
59
|
if (!id.startsWith(taggedPrefix)) {
|
|
46
60
|
throw new Error(`sqlite-table-where: tag prefix mismatch (id="${id}", taggedPrefix="${taggedPrefix}")`);
|
|
47
61
|
}
|
|
62
|
+
assertContained(dbPath, projectDir);
|
|
48
63
|
if (!fs.existsSync(dbPath)) {
|
|
49
64
|
return 'absent';
|
|
50
65
|
}
|
|
@@ -129,6 +129,7 @@ async function purgeRunInserts({ projectDir, runId, dryRun }) {
|
|
|
129
129
|
store: row.store,
|
|
130
130
|
id: row.id,
|
|
131
131
|
taggedPrefix: row.taggedPrefix,
|
|
132
|
+
projectDir, // thread through so path adapters can enforce containment (M60)
|
|
132
133
|
});
|
|
133
134
|
if (result === 'purged') {
|
|
134
135
|
purged.push(row);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.29.
|
|
3
|
+
"version": "3.29.11",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|