fix(exporters): allowlist trekresearch engine field and pin schema fixture agreement
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e1cf545a0c
commit
9a38500a63
4 changed files with 71 additions and 2 deletions
|
|
@ -25,8 +25,13 @@ const TREKBRIEF_ALLOWED = Object.freeze(new Set([
|
|||
]));
|
||||
|
||||
// Source: tests/fixtures/jsonl-schemas.md row 2 (trekresearch)
|
||||
// `engine` is a low-cardinality label (swarm|deep-research) emitted at
|
||||
// commands/trekresearch.md:533 and promised in prose (:570-572).
|
||||
// DENY BY OMISSION: question (free prose), project_dir + brief_path
|
||||
// (filesystem paths) are written into the jsonl but MUST NOT reach the
|
||||
// exporter.
|
||||
const TREKRESEARCH_ALLOWED = Object.freeze(new Set([
|
||||
'ts', 'slug', 'mode', 'scope', 'dimensions', 'agents_local',
|
||||
'ts', 'slug', 'mode', 'scope', 'engine', 'dimensions', 'agents_local',
|
||||
'agents_external', 'gemini_used', 'confidence', 'contradictions',
|
||||
'open_questions', 'profile', 'parallel_agents',
|
||||
'external_research_enabled', 'profile_source',
|
||||
|
|
|
|||
2
tests/fixtures/jsonl-schemas.md
vendored
2
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -20,7 +20,7 @@
|
|||
| schema_id | fields | writer_path | line_ref | v4.1 additive | PII |
|
||||
|-----------|--------|-------------|----------|---------------|-----|
|
||||
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, project_dir | commands/trekbrief.md (orchestrator-emit Phase 7) | trekbrief.md:657-672 | profile, phase_models, profile_source | none |
|
||||
| trekresearch-stats | ts, question, mode, scope, slug, project_dir, brief_path, dimensions, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | commands/trekresearch.md (orchestrator-emit Stats tracking) | trekresearch.md:388-410 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
|
||||
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | commands/trekresearch.md (orchestrator-emit Stats tracking) | trekresearch.md:521-547 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
|
||||
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | commands/trekplan.md (orchestrator-emit Phase 12) | trekplan.md:805-826 | profile, phase_models, parallel_agents, profile_source | none |
|
||||
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | commands/trekexecute.md (orchestrator-emit Phase 9) | trekexecute.md:1479-1494 | profile, phase_models, profile_source | none |
|
||||
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:64-86 | payload.profile, payload.phase_models, payload.profile_source | none |
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
POST_BASH_STATS_ALLOWED,
|
||||
EVENT_EMIT_PAYLOAD_ALLOWED,
|
||||
TOKEN_USAGE_ALLOWED,
|
||||
TREKRESEARCH_ALLOWED,
|
||||
} from '../../lib/exporters/field-allowlist.mjs';
|
||||
|
||||
// ---- path-validator: CWE-22 mitigation -------------------------------------
|
||||
|
|
@ -278,6 +279,43 @@ test('field-allowlist: token-usage INCLUDES numeric/label fields, EXCLUDES sessi
|
|||
assert.equal('cwd' in out, false, 'cwd MUST be stripped (CWE-212)');
|
||||
});
|
||||
|
||||
// ---- trekresearch allowlist: the `engine` field ----------------------------
|
||||
|
||||
test('field-allowlist: trekresearch INCLUDES engine, EXCLUDES question/project_dir/brief_path (two-sided)', () => {
|
||||
const record = {
|
||||
ts: '2026-08-09T12:00:00.000Z',
|
||||
question: 'which retrieval strategy survives contradiction?',
|
||||
mode: 'default',
|
||||
scope: 'both',
|
||||
engine: 'deep-research',
|
||||
slug: 'storm-upgrade',
|
||||
project_dir: '/Users/ktg/secret/project',
|
||||
brief_path: '/Users/ktg/secret/project/brief.md',
|
||||
dimensions: 4,
|
||||
agents_local: 7,
|
||||
agents_external: 4,
|
||||
gemini_used: false,
|
||||
confidence: 0.82,
|
||||
contradictions: 1,
|
||||
open_questions: 3,
|
||||
};
|
||||
const out = applyFieldAllowlist(record, 'trekresearch');
|
||||
// INCLUDED — low-cardinality label, emitted (trekresearch.md:533) and
|
||||
// promised in prose (:570-572); it was silently dropped before this pin.
|
||||
assert.equal('engine' in out, true, 'engine MUST be allowlisted — it is emitted and documented');
|
||||
assert.equal(out.engine, 'deep-research');
|
||||
assert.equal(out._schema_id, 'trekresearch');
|
||||
// EXCLUDED (CWE-212 boundary)
|
||||
assert.equal('question' in out, false, 'question MUST be stripped (prose, CWE-212)');
|
||||
assert.equal('project_dir' in out, false, 'project_dir MUST be stripped (path, CWE-212)');
|
||||
assert.equal('brief_path' in out, false, 'brief_path MUST be stripped (path, CWE-212)');
|
||||
});
|
||||
|
||||
test('field-allowlist: TREKRESEARCH_ALLOWED is frozen (drift-pin)', () => {
|
||||
assert.equal(Object.isFrozen(TREKRESEARCH_ALLOWED), true,
|
||||
'TREKRESEARCH_ALLOWED must be frozen — runtime mutation prevention');
|
||||
});
|
||||
|
||||
test('field-allowlist: null/undefined record handled safely', () => {
|
||||
assert.deepEqual(applyFieldAllowlist(null, 'trekplan'), {});
|
||||
assert.deepEqual(applyFieldAllowlist(undefined, 'trekplan'), {});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
import { resolveProfile, loadProfile } from '../../lib/profiles/resolver.mjs';
|
||||
import { STATES } from '../../lib/util/autonomy-gate.mjs';
|
||||
import { TREKRESEARCH_ALLOWED } from '../../lib/exporters/field-allowlist.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
|
@ -961,6 +962,31 @@ test('S15: profile tables encode each built-in yaml phase_models exactly', () =>
|
|||
}
|
||||
});
|
||||
|
||||
// STRUCTURAL pin: the exporter allowlist and the authoring fixture must agree.
|
||||
// `engine` was emitted, documented in prose, and still dropped at the export
|
||||
// boundary because nothing tied the two together. Derives one side from the
|
||||
// frozen Set, so it survives rewording of the fixture row.
|
||||
test('S74: every TREKRESEARCH_ALLOWED name is declared in the jsonl-schemas fixture row', () => {
|
||||
const row = read('tests/fixtures/jsonl-schemas.md')
|
||||
.split('\n')
|
||||
.find((l) => l.startsWith('| trekresearch-stats '));
|
||||
assert.ok(row, 'jsonl-schemas.md is missing the `trekresearch-stats` row');
|
||||
// Columns: '' | schema_id | fields | writer_path | line_ref | v4.1 additive | PII | ''
|
||||
const cells = row.split('|').map((c) => c.trim());
|
||||
// Both columns are required: profile/profile_source/parallel_agents live in
|
||||
// the `v4.1 additive` column only, so checking `fields` alone fails at once.
|
||||
const declared = new Set(
|
||||
[cells[2], cells[5]].flatMap((c) => c.split(',').map((f) => f.trim())).filter(Boolean),
|
||||
);
|
||||
for (const name of TREKRESEARCH_ALLOWED) {
|
||||
assert.ok(
|
||||
declared.has(name),
|
||||
`\`${name}\` is allowlisted in field-allowlist.mjs but absent from the fixture row's `
|
||||
+ '`fields` + `v4.1 additive` columns — fix the SOURCE, not this pin',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// --- S34 (V30) — economy is self-declared experimental until the cross-tier
|
||||
// Jaccard floor (0.55) is empirically calibrated (Step-17 calibration deferred
|
||||
// to v4.2). The status must be visible in BOTH the profile data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue