flow-projection-generator-data-integrity.test.mjs
162 lines 5.8 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 11 hours ago
1 /**
2 * Tier 5 — DATA INTEGRITY: fidelity round-trip and anti-drift diff proof.
3 *
4 * @see docs/FLOW-PROJECTION-GENERATOR-CONTRACT-7A-11.md §9–§10
5 */
6 import { describe, it, beforeEach, afterEach } from 'node:test';
7 import assert from 'node:assert/strict';
8 import fs from 'node:fs';
9 import path from 'node:path';
10 import { fileURLToPath } from 'node:url';
11 import {
12 projectFlow,
13 detectDrift,
14 } from '../lib/flow/projection-generator.mjs';
15 import { handleFlowProjectRequest } from '../lib/flow/flow-handlers.mjs';
16 import { saveFlowStore, validateFlowBundle, buildFlowStepId } from '../lib/flow/flow-store.mjs';
17 import { getRepoRoot } from '../lib/repo-root.mjs';
18
19 const __dirname = path.dirname(fileURLToPath(import.meta.url));
20 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-projection-integrity');
21 const starterDir = path.join(getRepoRoot(), 'flows/starter');
22 const handoverBundle = JSON.parse(
23 fs.readFileSync(path.join(starterDir, 'flow_overseer_handover.json'), 'utf8'),
24 );
25
26 const STEP_FIELDS = [
27 'owned_job',
28 'instruction',
29 'trigger',
30 'when_not_to_run',
31 'requires',
32 'boundaries',
33 'skill_refs',
34 'inputs',
35 'outputs',
36 'output_shape',
37 'verification',
38 ];
39
40 /**
41 * @param {string} text
42 * @returns {string}
43 */
44 function normalizeRenderedForFieldCheck(text) {
45 return text.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
46 }
47
48 /**
49 * @param {string} rendered
50 * @param {string[]} dropped
51 * @param {object} step
52 */
53 function assertFieldExpressedOrDropped(rendered, dropped, step) {
54 const normalized = normalizeRenderedForFieldCheck(rendered);
55 for (const field of STEP_FIELDS) {
56 const value = step[field];
57 const present =
58 (typeof value === 'string' && value.trim()) ||
59 (Array.isArray(value) && value.length > 0) ||
60 (field === 'verification' && value && typeof value === 'object');
61 if (!present) continue;
62 const inDropped = dropped.includes(field) || dropped.includes('verification.evidence_required');
63 const inRendered =
64 (field === 'owned_job' && normalized.includes(step.owned_job)) ||
65 (field === 'instruction' && normalized.includes(step.instruction)) ||
66 (field === 'trigger' && normalized.includes(step.trigger)) ||
67 (field === 'when_not_to_run' && normalized.includes(step.when_not_to_run)) ||
68 (field === 'requires' &&
69 Array.isArray(step.requires) &&
70 step.requires.every((r) => normalized.includes(r.id))) ||
71 (field === 'boundaries' &&
72 step.boundaries.every((b) => normalized.includes(b))) ||
73 (field === 'skill_refs' &&
74 Array.isArray(step.skill_refs) &&
75 step.skill_refs.every((r) => normalized.includes(r.id))) ||
76 (field === 'inputs' &&
77 step.inputs.every((i) => normalized.includes(i.name))) ||
78 (field === 'outputs' &&
79 step.outputs.every((o) => normalized.includes(o.name))) ||
80 (field === 'output_shape' && normalized.includes(step.output_shape)) ||
81 (field === 'verification' && normalized.includes(step.verification.description));
82 assert.ok(inRendered || inDropped, `field ${field} neither rendered nor dropped`);
83 }
84 }
85
86 describe('Flow projection — data integrity', () => {
87 const dataDir = path.join(tmpRoot, 'data');
88 const vaultId = 'default';
89
90 beforeEach(() => {
91 fs.rmSync(tmpRoot, { recursive: true, force: true });
92 fs.mkdirSync(dataDir, { recursive: true });
93 });
94
95 afterEach(() => {
96 fs.rmSync(tmpRoot, { recursive: true, force: true });
97 });
98
99 it('every present step field is rendered or listed in dropped_fields', () => {
100 const projection = projectFlow(handoverBundle.flow, handoverBundle.steps, {
101 harness: 'cursor_rule',
102 });
103 const dropped = projection.fidelity.dropped_fields;
104 for (const step of handoverBundle.steps) {
105 assertFieldExpressedOrDropped(projection.rendered, dropped, step);
106 }
107 });
108
109 it('anti-drift: canonical change is the only diff after regenerate', () => {
110 const v1 = structuredClone(handoverBundle);
111 const rendered1 = projectFlow(v1.flow, v1.steps, { harness: 'cli_runbook' }).rendered;
112
113 const v2 = structuredClone(handoverBundle);
114 v2.flow.version = '0.2.0';
115 v2.steps[0].verification.description = 'Tightened verification for anti-drift proof.';
116 const rendered2 = projectFlow(v2.flow, v2.steps, { harness: 'cli_runbook' }).rendered;
117
118 assert.notEqual(rendered1, rendered2);
119 assert.ok(rendered2.includes('Tightened verification for anti-drift proof.'));
120 assert.ok(!rendered1.includes('Tightened verification for anti-drift proof.'));
121 assert.deepEqual(detectDrift(rendered2, rendered2), { drift: false, reason: 'clean' });
122 });
123
124 it('delete and regenerate reproduces byte-for-byte', () => {
125 const first = projectFlow(handoverBundle.flow, handoverBundle.steps, {
126 harness: 'cli_runbook',
127 }).rendered;
128 const second = projectFlow(handoverBundle.flow, handoverBundle.steps, {
129 harness: 'cli_runbook',
130 }).rendered;
131 assert.equal(first, second);
132 });
133
134 it('flow_version in projection matches canonical source version', () => {
135 const validated = validateFlowBundle(handoverBundle);
136 assert.equal(validated.ok, true);
137 saveFlowStore(dataDir, {
138 vaults: {
139 [vaultId]: {
140 flows: [validated.flow, { ...validated.flow, version: '0.2.0', updated: '2026-06-21T00:00:00Z' }],
141 steps: validated.steps,
142 runs: [],
143 candidates: [],
144 projections: [],
145 },
146 },
147 });
148 const pinned = handleFlowProjectRequest({
149 dataDir,
150 vaultId,
151 flowId: 'flow_overseer_handover',
152 harness: 'cli_runbook',
153 version: '0.1.0',
154 visibleScopes: new Set(['project']),
155 generatedAt: '2026-06-20T00:00:00Z',
156 });
157 assert.equal(pinned.ok, true);
158 assert.equal(pinned.payload.projection.flow_version, '0.1.0');
159 assert.equal(pinned.payload.staleness.stale, true);
160 assert.equal(pinned.payload.staleness.latest_version, '0.2.0');
161 });
162 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 11 hours ago