fix: pre-landing review fixes

sealed.label_join: a hex object key is an identifier of the object it holds; label_keys and identifier_keys refuse dotted names; register the off golden as a free fixture.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 21:05:18 +00:00
1 parent 3fef3423ab
commit 964bd799ea
5 files changed
+25 -14

No files matched your search

@@ -1,7 +1,7 @@
**The sealed guard can now refuse a published record that puts a label next to an identifier.** gstack-evals hides which test units are decoys. Its /ship on garrytan/gstack-evals#99 published a panel where each unit's `stratum`, `tags` and `decoy_set` sat next to its `head`, `tree_sha256`, `plan_bytes` and `files`, values a model under test can compute from its own copy of the work. A model could look itself up and learn it was the decoy. No answer leaked, so `gstack-pregate sealed` said `clear` (GSTA-2, GST-620). The new rule is opt-in. A repo that sets `sealed.label_keys` gets it, and every other repo sees the same output as before, byte for byte.
### Added
- **`sealed.label_keys` and `sealed.identifier_keys` in `.gstack/pregate.json`.** With `label_keys` set, `sealed.label_join` checks every .json, .jsonl, .ndjson, .csv, .tsv and Markdown pipe table a published commit adds, modifies, renames or copies. A record that holds a label field and an identifier (a field matching an `identifier_keys` glob, by default `head`, `*_sha256`, `*_bytes`, `files` and similar names, or any 7 to 64 character hex string) is a join. The guard fingerprints each join in memory and compares it with the parent's version of the file, so unchanged rows and reformatting stay clear while a replaced, added or widened row refuses even when the row count stays the same. Findings name the file, the commit and the field names (`keys=stratum,tags,head,plan_bytes`), never the values, on stderr, in `--json` and in the pre-push hook.
- **`sealed.label_keys` and `sealed.identifier_keys` in `.gstack/pregate.json`.** With `label_keys` set, `sealed.label_join` checks every .json, .jsonl, .ndjson, .csv, .tsv and Markdown pipe table a published commit adds, modifies, renames or copies. A record that holds a label field and an identifier (a field matching an `identifier_keys` glob, by default `head`, `*_sha256`, `*_bytes`, `files` and similar names, or any 7 to 64 character hex string, as a value or as the key of an object) is a join. The guard fingerprints each join in memory and compares it with the parent's version of the file, so unchanged rows and reformatting stay clear while a replaced, added or widened row refuses even when the row count stays the same. Findings name the file, the commit and the field names (`keys=stratum,tags,head,plan_bytes`), never the values, on stderr, in `--json` and in the pre-push hook.
- **`sealed.label_join_unchecked`.** A supported file over 4 MiB, one that does not parse, one nested deeper than 64 levels, or a blob git cannot read refuses instead of passing unread. The `SEALED:` line counts these as `unchecked=<n>`. Like every sealed finding, it can be waived through the base branch's `.gstack/sealed-allow.tsv`; `allow_paths` never exempt either rule.
- **The policy only tightens.** Both lists are the union of the base branch's and the pushed policy's, each resolved to its default first, so a pushed config can turn the rule on or widen it but cannot turn it off or narrow it.
@@ -10,4 +10,4 @@
### Tests
- `test/sealed-guard.test.ts`: the GSTA-2 panel refuses through `gstack-pregate sealed`, `--json` and the pre-push hook, and warns on a private destination, with no value in any output. It failed on main. A golden captured from main 2d1b55d76 covers seven pushes (refused, clear, exempt, waived, warned, error, no policy) without `label_keys`; stdout, stderr, `--json`, the hook's stderr and every exit code match it byte for byte.
- `test/sealed-join.test.ts`: each format, human table headers, hex values under unlisted keys, decimal strings that stay clear, replacement at the same count, delete plus add, an added identifier field, labels then identifiers a commit later, renames, merges, five unchecked cases, waivers, `allow_paths`, pushed attempts to narrow or disable the rule, and `label_keys: []` matching an absent key.
- `test/sealed-join.test.ts`: each format, human table headers, hex values under unlisted keys, maps keyed by sha, dotted key names refused in the config, decimal strings that stay clear, replacement at the same count, delete plus add, an added identifier field, labels then identifiers a commit later, renames, merges, five unchecked cases, waivers, `allow_paths`, pushed attempts to narrow or disable the rule, and `label_keys: []` matching an absent key.
+8 -4
View File
@@ -1007,8 +1007,11 @@ gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--js
is `head`, `head_sha`, `sha`, `commit`, `tree`, `tree_sha*`, `*_sha`,
`*_sha256`, `sha256`, `digest`, `*_digest`, `hash`, `*_hash`,
`*_bytes`, `bytes`, `size`, `files`, `lines`), or any string value of 7
to 64 hex characters with at least one digit and one letter a to f. Numbers
count only through the field name. For each supported file a commit adds,
to 64 hex characters with at least one digit and one letter a to f. An
object key of that shape counts as an identifier of the object it holds
(shown as `(key)`), so a map keyed by sha joins too. Numbers count only
through the field name. Both lists take bare field names: a nested field
matches by its own name (`stratum`, never `meta.stratum`). For each supported file a commit adds,
modifies, renames or copies, the guard fingerprints every joined record
(sha256 over its label and identifier fields and their values, kept in
memory) and compares the set with the parent's version of the file (the
@@ -1026,8 +1029,9 @@ gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--js
but never turn it off or narrow it. Without `label_keys` the guard reads no
file contents and its output is unchanged. Known limits: joins across files,
joins by reference inside a file (a `decoy_set` list naming units described
elsewhere), columnar JSON (`columns` plus `rows`), labels encoded in file
names, and prose.
elsewhere), a map from an identifier straight to a label value with no
label field name, columnar JSON (`columns` plus `rows`), labels encoded in
file names, and prose.
- **Version skew.** A gstack older than the release that adds a `sealed` key
refuses it as unknown, so every push and `gstack-pregate run` in the
repository fail on that machine. `allow_paths` and `case_insensitive` came
+7 -7
View File
@@ -34,10 +34,10 @@ type Joins = Map<string, { labels: string[]; ids: string[] }>;
export const normalizeKey = (k: string) => k.trim().toLowerCase().replace(/[ -]/g, '_');
export function labelKeyError(s: string): string | null {
return s.trim() && s.length <= MAX_KEY && !/[*?[\]{}]/.test(s) ? null : `must be a field name of 1-${MAX_KEY} characters with no wildcard`;
return s.trim() && s.length <= MAX_KEY && !/[*?[\]{}.]/.test(s) ? null : `must be a bare field name of 1-${MAX_KEY} characters with no wildcard or dot (meta.stratum matches as stratum)`;
}
export function identifierKeyError(s: string): string | null {
return s.trim() && s.length <= MAX_KEY && !/[?[\]{}]/.test(s) ? null : `must be a field name of 1-${MAX_KEY} characters; * is the only wildcard`;
return s.trim() && s.length <= MAX_KEY && !/[?[\]{}.]/.test(s) ? null : `must be a bare field name of 1-${MAX_KEY} characters with no dot; * is the only wildcard`;
}
const present = (v: unknown) => typeof v === 'number' || (typeof v === 'string' && v.trim() !== '') || (Array.isArray(v) && v.length > 0);
@@ -59,21 +59,21 @@ function classifier(p: JoinPolicy): (fields: Field[]) => { fp: string; labels: s
};
}
/** Every JSON object at any depth as one record: its own fields plus one level of nested object fields (`meta.head`); false past the depth cap. */
/** Every JSON object at any depth as one record: its own fields plus one level of nested object fields (`meta.head`), and the hex key that holds it as `(key)`; false past the depth cap. */
function jsonRecords(root: unknown, out: Field[][]): boolean {
const field = (name: string, value: unknown, key: string): Field => ({ name, key: normalizeKey(key), value });
const stack: Array<[unknown, number]> = [[root, 0]];
const stack: Array<[unknown, number, string?]> = [[root, 0]];
while (stack.length) {
const [v, depth] = stack.pop()!;
const [v, depth, heldBy] = stack.pop()!;
if (!v || typeof v !== 'object') continue;
if (depth > MAX_DEPTH) return false;
if (Array.isArray(v)) { for (const e of v) stack.push([e, depth + 1]); continue; }
const fields: Field[] = [];
const fields: Field[] = heldBy && HEX_ID.test(heldBy) ? [field('(key)', heldBy, '(key)')] : [];
for (const [k, val] of Object.entries(v)) {
const name = normalizeKey(k);
fields.push(field(name, val, k));
if (val && typeof val === 'object' && !Array.isArray(val)) for (const [k2, v2] of Object.entries(val)) fields.push(field(`${name}.${normalizeKey(k2)}`, v2, k2));
stack.push([val, depth + 1]);
stack.push([val, depth + 1, k]);
}
out.push(fields);
}
+1
View File
@@ -199,6 +199,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
'test/fixtures/review-ledger/**': ['test/review-dispatch.test.ts'],
'test/fixtures/review-design-lite-reports-ci-36633323521.json': ['test/review-finalization-budget.test.ts'],
'test/fixtures/review-gate-integrity-opus-reports.json': ['test/gate-integrity-templates.test.ts'],
'test/fixtures/sealed-guard-off-golden.json': ['test/sealed-guard.test.ts'],
'test/fixtures/session-update-heartbeat.sh': ['test/session-update-heartbeat.test.ts'],
'test/fixtures/shard-cli-parity/**': ['test/shard-cli-parity.test.ts'],
'test/fixtures/shard-equivalence/**': ['test/shard-engine-equivalence.test.ts'],
+7 -1
View File
@@ -84,6 +84,9 @@ describe('formats and matching', () => {
'g.json': '[{"stratum":"decoy"},{"head":"5b7db3aa"}]\n',
'h.md': '| head | size |\n|---|---|\n| 5b7db3a | 3 |\n',
'i.txt': '{"stratum":"decoy","head":"5b7db3a"}\n',
'j.json': `{"units":{"${HEAD_A}":{"stratum":"decoy","tags":["t"]}}}\n`,
'k.jsonl': `{"${HEAD_B}":{"stratum":"seeded"}}\n`,
'l.json': '{"units":{"unit-one":{"stratum":"decoy"}}}\n',
});
expect(found(s).sort((a, b) => a.file.localeCompare(b.file))).toEqual([
{ rule: 'sealed.label_join', file: 'a.jsonl', commit: sha, keys: ['stratum', 'meta.head'] },
@@ -91,6 +94,8 @@ describe('formats and matching', () => {
{ rule: 'sealed.label_join', file: 'c.tsv', commit: sha, keys: ['stratum', 'tree_sha256'] },
{ rule: 'sealed.label_join', file: 'd.md', commit: sha, keys: ['stratum', 'head', 'tree_sha_256'] },
{ rule: 'sealed.label_join', file: 'e.json', commit: sha, keys: ['tags', 'id'] },
{ rule: 'sealed.label_join', file: 'j.json', commit: sha, keys: ['stratum', 'tags', '(key)'] },
{ rule: 'sealed.label_join', file: 'k.jsonl', commit: sha, keys: ['stratum', '(key)'] },
]);
});
@@ -98,11 +103,12 @@ describe('formats and matching', () => {
expect(joinFindings(os.tmpdir(), ['0'.repeat(40)], { label_keys: [], identifier_keys: DEFAULT_IDENTIFIER_KEYS })).toEqual([]);
});
test('label_keys must be plain field names and identifier_keys allow only *; both must be string arrays', () => {
test('label_keys must be bare field names and identifier_keys allow only *; neither takes a dotted path; both must be string arrays', () => {
expect(sealedErrors({ label_keys: ['stratum'], identifier_keys: ['*_sha', 'head'] })).toEqual([]);
expect(sealedErrors({ label_keys: ['strat*'] }).map(e => e.path)).toEqual(['sealed.label_keys[0]']);
expect(sealedErrors({ identifier_keys: ['he?d', ''] }).map(e => e.path)).toEqual(['sealed.identifier_keys[0]', 'sealed.identifier_keys[1]']);
expect(sealedErrors({ label_keys: 'stratum' })).toEqual([{ path: 'sealed.label_keys', message: 'must be an array of strings' }]);
expect(sealedErrors({ label_keys: ['meta.stratum'], identifier_keys: ['meta.head'] }).map(e => e.path)).toEqual(['sealed.label_keys[0]', 'sealed.identifier_keys[0]']);
});
});