@besales/ops-framework 0.1.17 → 0.1.20
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 +18 -0
- package/README.md +2 -0
- package/bin/initiative.mjs +163 -1
- package/bin/initiative.test.mjs +218 -0
- package/bin/lib/check-context-utils.mjs +251 -2
- package/bin/lib/check-context-utils.test.mjs +260 -0
- package/bin/lib/task-manifest-utils.test.mjs +22 -0
- package/bin/run-check.mjs +92 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.20
|
|
4
|
+
|
|
5
|
+
- Added deterministic task Check gates for schema/migration plans: a real disposable/scratch database apply path is required before external Check, and Verify/Human Gate evidence must show a successful apply/migrate/psql run rather than static SQL review only.
|
|
6
|
+
- Added audit/entity_history ambiguity gates: plans and initiative synthesis must explicitly choose a functional DB trigger writer model or an application-code writer model.
|
|
7
|
+
- Added a draft-storage synthesis gate for Variant-A/draft-only apply contracts so `proposed_changes` must explicitly support deferred fact domains with entity/change type, JSON payload, source quote, confidence, financial event, related person and conflict metadata.
|
|
8
|
+
|
|
9
|
+
## 0.1.19
|
|
10
|
+
|
|
11
|
+
- Strengthened deterministic Check preflight so external checker calls are skipped when the plan still has placeholder execution metadata, missing verification ladder or missing standards alignment.
|
|
12
|
+
- Tightened O3 optimization gating: plans must include a representative measurement path or explicit blocked/human-owned fallback before external Check.
|
|
13
|
+
- Added `check-timeline.json` telemetry for Check runs, including deterministic blocks, LLM input sizing, cache hits, provider start/completion/failure and context overflow events.
|
|
14
|
+
|
|
15
|
+
## 0.1.18
|
|
16
|
+
|
|
17
|
+
- Added work-package dependency metadata (`Depends on`, `Unblocks`, `Can run parallel with`, `Sequencing notes`) to new initiative work packages.
|
|
18
|
+
- Made `initiative-next` dependency-aware and prevented materialization when pending work packages are blocked by unmet dependencies.
|
|
19
|
+
- Added a work-package readiness gate that blocks materialization when a work package covers `REQ-*` items but has only process-level acceptance.
|
|
20
|
+
|
|
3
21
|
## 0.1.17
|
|
4
22
|
|
|
5
23
|
- Broadened the schema compatibility gate so it blocks any schema/foundation requirement that omits fields required by approved dependent requirements, even when the schema text is not written as an explicit closed list.
|
package/README.md
CHANGED
|
@@ -249,6 +249,8 @@ Initiative
|
|
|
249
249
|
|
|
250
250
|
Work packages are materialized as normal tasks, so `brief/research/plan/check/execute/verify/retrospective/learning` still works at the audit boundary. Slices are not separate tasks; they use fast execution, micro-verify and slice evidence inside the work-package task. Create a separate task only when a slice triggers scope, risk, architecture or human-approval escalation.
|
|
251
251
|
|
|
252
|
+
Work packages can declare `Depends on`, `Unblocks`, `Can run parallel with` and `Sequencing notes`. `initiative-next` uses `Depends on` to avoid materializing a package before required packages are completed. If a work package lists `REQ-*` coverage, its `Acceptance` must include requirement-level done criteria or explicit `REQ-*` acceptance references; process-only acceptance blocks materialization.
|
|
253
|
+
|
|
252
254
|
For source docs, use intake before work-package planning:
|
|
253
255
|
|
|
254
256
|
```text
|
package/bin/initiative.mjs
CHANGED
|
@@ -202,12 +202,14 @@ export function initiativeNext({
|
|
|
202
202
|
force = false,
|
|
203
203
|
} = {}) {
|
|
204
204
|
const status = initiativeStatus({ projectRoot, initiativeId });
|
|
205
|
-
const
|
|
205
|
+
const readiness = buildWorkPackageReadiness(status.workPackages);
|
|
206
|
+
const next = readiness.find((item) => item.ready)?.workPackage || null;
|
|
206
207
|
if (!next) {
|
|
207
208
|
return {
|
|
208
209
|
initiativeId,
|
|
209
210
|
next: null,
|
|
210
211
|
materializedTask: null,
|
|
212
|
+
readiness,
|
|
211
213
|
};
|
|
212
214
|
}
|
|
213
215
|
let materializedTask = null;
|
|
@@ -229,6 +231,7 @@ export function initiativeNext({
|
|
|
229
231
|
initiativeId,
|
|
230
232
|
next,
|
|
231
233
|
materializedTask,
|
|
234
|
+
readiness,
|
|
232
235
|
};
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -1035,7 +1038,9 @@ function buildSynthesisReadinessGates({ requirements, sourceDocs }) {
|
|
|
1035
1038
|
});
|
|
1036
1039
|
addScopeStorageConflictGate({ gates, sourceText, requirementText });
|
|
1037
1040
|
addApplyContractGate({ gates, sourceText, requirementText });
|
|
1041
|
+
addDraftStorageShapeGate({ gates, sourceText, requirementText });
|
|
1038
1042
|
addSchemaCompatibilityGate({ gates, sourceText, requirements });
|
|
1043
|
+
addAuditWriterModelGate({ gates, sourceText, requirementText });
|
|
1039
1044
|
addSchemaAttributionWarningGate({ gates, sourceText, requirements });
|
|
1040
1045
|
addSequencingGate({ gates, sourceText, requirementText });
|
|
1041
1046
|
addStrongerSourceRefGate({ gates, sourceDocs, requirements });
|
|
@@ -1102,6 +1107,65 @@ function addApplyContractGate({ gates, sourceText, requirementText }) {
|
|
|
1102
1107
|
}
|
|
1103
1108
|
}
|
|
1104
1109
|
|
|
1110
|
+
function addDraftStorageShapeGate({ gates, sourceText, requirementText }) {
|
|
1111
|
+
const combinedText = `${requirementText}\n${sourceText}`;
|
|
1112
|
+
const hasDraftOnlyContract = /(variant\s*a|draft[- ]only|draft|proposed only|not applied|только proposed|чернов)/i.test(combinedText);
|
|
1113
|
+
const hasDeferredFactDomains = /(commitments?|decisions?|risks?|projects?|financial_event|related_person_id|has_conflict)/i.test(combinedText);
|
|
1114
|
+
const mentionsProposedChanges = /proposed_changes?|proposedchange|proposed change/i.test(combinedText);
|
|
1115
|
+
if (!hasDraftOnlyContract || !hasDeferredFactDomains || !mentionsProposedChanges) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const requiredFields = [
|
|
1120
|
+
['entity_type', /entity_type/i],
|
|
1121
|
+
['change_type', /change_type/i],
|
|
1122
|
+
['proposed_value JSONB', /(proposed_value|payload).{0,40}(jsonb|json)/i],
|
|
1123
|
+
['source_quote/source_quote_raw', /source_quote(?:_raw)?/i],
|
|
1124
|
+
['confidence', /confidence/i],
|
|
1125
|
+
['financial_event_type', /financial_event_type/i],
|
|
1126
|
+
['related_person_id', /related_person_id/i],
|
|
1127
|
+
['has_conflict', /has_conflict/i],
|
|
1128
|
+
];
|
|
1129
|
+
const missing = requiredFields
|
|
1130
|
+
.filter(([, pattern]) => !pattern.test(requirementText))
|
|
1131
|
+
.map(([name]) => name);
|
|
1132
|
+
|
|
1133
|
+
if (missing.length) {
|
|
1134
|
+
gates.push({
|
|
1135
|
+
id: 'draft-storage-proposed-changes-shape',
|
|
1136
|
+
severity: 'critical',
|
|
1137
|
+
blocking: true,
|
|
1138
|
+
title: 'Draft-only apply contract lacks proposed_changes storage shape',
|
|
1139
|
+
issue: `Variant/draft-only requirements rely on proposed_changes for deferred fact domains, but synthesized requirements do not explicitly cover ${missing.join(', ')}.`,
|
|
1140
|
+
action: 'Before WP planning, add acceptance that `proposed_changes` can store draft commitments/decisions/risks/projects/facts with entity_type, change_type, proposed_value JSONB, source quote fields, confidence, financial_event_type, related_person_id and conflict metadata, or narrow the draft output scope.',
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function addAuditWriterModelGate({ gates, sourceText, requirementText }) {
|
|
1146
|
+
const combinedText = `${requirementText}\n${sourceText}`;
|
|
1147
|
+
const mentionsAudit = /(entity_history|audit|before\/after|before and after|log_entity_change|app\.current_user_id|attribution)/i.test(combinedText);
|
|
1148
|
+
if (!mentionsAudit) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const triggerModel = /(trigger|log_entity_change|before\/after|before and after|current_setting\('app\.current_user_id'\)|триггер)/i.test(requirementText);
|
|
1152
|
+
const appModel = /(application[- ]code writer|app(?:lication)? writes|apply[- ]code writes|manual writer|writes entity_history manually|пишет entity_history|прикладн)/i.test(requirementText);
|
|
1153
|
+
const ambiguous = /(trigger|триггер).{0,80}\b(or|или)\b.{0,80}(application|app|manual|код|вручн)/i.test(combinedText)
|
|
1154
|
+
|| /(application|app|manual|код|вручн).{0,80}\b(or|или)\b.{0,80}(trigger|триггер)/i.test(combinedText)
|
|
1155
|
+
|| /(table only|only table|таблиц[ауы].{0,80}без триггер|helper pattern only|entity_history plus helper pattern|helper pattern)/i.test(combinedText);
|
|
1156
|
+
if (!ambiguous && (triggerModel || appModel)) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
gates.push({
|
|
1160
|
+
id: 'audit-writer-model-ambiguous',
|
|
1161
|
+
severity: 'needs_decision',
|
|
1162
|
+
blocking: true,
|
|
1163
|
+
title: 'Audit history writer model is ambiguous',
|
|
1164
|
+
issue: 'Requirements mention audit/entity_history/attribution, but do not decide whether audit rows are written by functional DB triggers or by application/apply code.',
|
|
1165
|
+
action: 'Before WP-001 planning, choose the audit writer model explicitly. For DB-trigger audit, require `log_entity_change`, trigger attachment to core tables and `app.current_user_id`; for app-code audit, require the exact apply-code writer contract and covered entities.',
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1105
1169
|
function addSchemaCompatibilityGate({ gates, sourceText, requirements }) {
|
|
1106
1170
|
const schemaReq = findSchemaRequirement(requirements);
|
|
1107
1171
|
if (!schemaReq) {
|
|
@@ -1327,6 +1391,7 @@ function listWorkPackages(initiativeDir) {
|
|
|
1327
1391
|
|
|
1328
1392
|
function readWorkPackage(filePath, fallbackId) {
|
|
1329
1393
|
const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
1394
|
+
const dependencies = readWorkPackageDependencies(content);
|
|
1330
1395
|
return {
|
|
1331
1396
|
id: readInlineField(content, 'ID') || fallbackId,
|
|
1332
1397
|
title: readInlineField(content, 'Title') || humanizeSlug(fallbackId.replace(/^WP-\d{3}-/, '')),
|
|
@@ -1334,11 +1399,89 @@ function readWorkPackage(filePath, fallbackId) {
|
|
|
1334
1399
|
task: readInlineField(content, 'Task') || '',
|
|
1335
1400
|
mode: readInlineField(content, 'Mode') || 'standard_work_package',
|
|
1336
1401
|
goal: readSection(content, 'Goal') || '',
|
|
1402
|
+
requirementsCoverage: readBulletsFromSection(content, 'Requirements Coverage'),
|
|
1337
1403
|
slices: readBulletsFromSection(content, 'Slices'),
|
|
1404
|
+
acceptance: readBulletsFromSection(content, 'Acceptance'),
|
|
1405
|
+
dependencies,
|
|
1338
1406
|
path: filePath,
|
|
1339
1407
|
};
|
|
1340
1408
|
}
|
|
1341
1409
|
|
|
1410
|
+
function readWorkPackageDependencies(content) {
|
|
1411
|
+
const section = readSection(content, 'Dependencies');
|
|
1412
|
+
return {
|
|
1413
|
+
dependsOn: readDependencyField(section, 'Depends on'),
|
|
1414
|
+
unblocks: readDependencyField(section, 'Unblocks'),
|
|
1415
|
+
parallelWith: readDependencyField(section, 'Can run parallel with'),
|
|
1416
|
+
sequencingNotes: readDependencyField(section, 'Sequencing notes'),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function readDependencyField(section, field) {
|
|
1421
|
+
if (!section) {
|
|
1422
|
+
return [];
|
|
1423
|
+
}
|
|
1424
|
+
const match = new RegExp(`^-\\s+${escapeRegExp(field)}:\\s*(.*)$`, 'im').exec(section);
|
|
1425
|
+
if (!match) {
|
|
1426
|
+
return [];
|
|
1427
|
+
}
|
|
1428
|
+
return splitDependencyRefs(match[1]);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function splitDependencyRefs(value) {
|
|
1432
|
+
return String(value || '')
|
|
1433
|
+
.split(/[,;]/)
|
|
1434
|
+
.map((item) => item.trim())
|
|
1435
|
+
.filter((item) => item && !/^\(?none\)?$/i.test(item) && !/^\[?fill in\]?$/i.test(item));
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function buildWorkPackageReadiness(workPackages) {
|
|
1439
|
+
const byId = new Map(workPackages.map((wp) => [wp.id, wp]));
|
|
1440
|
+
return workPackages
|
|
1441
|
+
.filter((wp) => ['pending', 'ready'].includes(wp.status))
|
|
1442
|
+
.map((wp) => {
|
|
1443
|
+
const blockers = [];
|
|
1444
|
+
for (const dependencyId of wp.dependencies.dependsOn) {
|
|
1445
|
+
const dependency = byId.get(dependencyId);
|
|
1446
|
+
if (!dependency) {
|
|
1447
|
+
blockers.push(`Dependency ${dependencyId} is missing.`);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (!workPackageDependencySatisfied(dependency)) {
|
|
1451
|
+
blockers.push(`Dependency ${dependencyId} is ${dependency.status || 'unknown'}, not completed.`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (workPackageAcceptanceIsProcessOnly(wp)) {
|
|
1455
|
+
blockers.push('Acceptance is process-only while Requirements Coverage is present; add requirement-level done criteria before materialize.');
|
|
1456
|
+
}
|
|
1457
|
+
return {
|
|
1458
|
+
workPackage: wp,
|
|
1459
|
+
ready: blockers.length === 0,
|
|
1460
|
+
blockers,
|
|
1461
|
+
};
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function workPackageDependencySatisfied(workPackage) {
|
|
1466
|
+
return /^(done|complete|completed|verified|closed)$/i.test(workPackage.status || '');
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function workPackageAcceptanceIsProcessOnly(workPackage) {
|
|
1470
|
+
const coverageText = workPackage.requirementsCoverage.join('\n');
|
|
1471
|
+
if (!/\bREQ-\d{3}\b/.test(coverageText)) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
const acceptance = workPackage.acceptance;
|
|
1475
|
+
if (!acceptance.length) {
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
const acceptanceText = acceptance.join('\n');
|
|
1479
|
+
if (/\bREQ-\d{3}\b/.test(acceptanceText)) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
return acceptance.every((item) => /(verify completed|completed verify|slice ledger|learning closeout|task closeout|execution\.md)/i.test(item));
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1342
1485
|
function updateWorkPackageStatus({ workPackagePath, status, taskId }) {
|
|
1343
1486
|
const content = fs.readFileSync(workPackagePath, 'utf8');
|
|
1344
1487
|
let updated = content
|
|
@@ -1438,6 +1581,17 @@ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mo
|
|
|
1438
1581
|
'- Excluded:',
|
|
1439
1582
|
'- Escalate to separate task if:',
|
|
1440
1583
|
'',
|
|
1584
|
+
'## Requirements Coverage',
|
|
1585
|
+
'',
|
|
1586
|
+
'- REQ-000: Add covered requirement and keep acceptance aligned with requirement-level done criteria.',
|
|
1587
|
+
'',
|
|
1588
|
+
'## Dependencies',
|
|
1589
|
+
'',
|
|
1590
|
+
'- Depends on: (none)',
|
|
1591
|
+
'- Unblocks: (none)',
|
|
1592
|
+
'- Can run parallel with: (none)',
|
|
1593
|
+
'- Sequencing notes: (none)',
|
|
1594
|
+
'',
|
|
1441
1595
|
'## Slices',
|
|
1442
1596
|
'',
|
|
1443
1597
|
'- Slice 1: Define the first implementation slice.',
|
|
@@ -1446,6 +1600,7 @@ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mo
|
|
|
1446
1600
|
'',
|
|
1447
1601
|
'## Acceptance',
|
|
1448
1602
|
'',
|
|
1603
|
+
'- Covered REQ-* acceptance criteria are satisfied or explicitly deferred with human approval.',
|
|
1449
1604
|
'- Work-package task has completed Verify.',
|
|
1450
1605
|
'- Slice ledger is recorded in execution.md.',
|
|
1451
1606
|
'- Learning closeout is completed before task closeout.',
|
|
@@ -1562,6 +1717,13 @@ function printInitiativeNext(result) {
|
|
|
1562
1717
|
console.log(`Initiative next: ${result.initiativeId}`);
|
|
1563
1718
|
if (!result.next) {
|
|
1564
1719
|
console.log('- no pending work packages');
|
|
1720
|
+
const blocked = (result.readiness || []).filter((item) => !item.ready);
|
|
1721
|
+
for (const item of blocked) {
|
|
1722
|
+
console.log(`- blocked: ${item.workPackage.id}`);
|
|
1723
|
+
for (const blocker of item.blockers) {
|
|
1724
|
+
console.log(` - ${blocker}`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1565
1727
|
return;
|
|
1566
1728
|
}
|
|
1567
1729
|
console.log(`- workPackage: ${result.next.id}`);
|
package/bin/initiative.test.mjs
CHANGED
|
@@ -76,6 +76,110 @@ describe('initiative framework', () => {
|
|
|
76
76
|
expect(workPackage).toContain('Task: TASK-001-foundation');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it('selects the next work package with dependency readiness', () => {
|
|
80
|
+
const root = makeProject();
|
|
81
|
+
createInitiative({
|
|
82
|
+
projectRoot: root,
|
|
83
|
+
initiativeId: 'delivery-os-mvp',
|
|
84
|
+
title: 'Delivery OS MVP',
|
|
85
|
+
});
|
|
86
|
+
addWorkPackage({
|
|
87
|
+
projectRoot: root,
|
|
88
|
+
initiativeId: 'delivery-os-mvp',
|
|
89
|
+
workPackageId: 'WP-001-foundation',
|
|
90
|
+
title: 'Foundation',
|
|
91
|
+
});
|
|
92
|
+
addWorkPackage({
|
|
93
|
+
projectRoot: root,
|
|
94
|
+
initiativeId: 'delivery-os-mvp',
|
|
95
|
+
workPackageId: 'WP-003-process-meeting',
|
|
96
|
+
title: 'Process Meeting',
|
|
97
|
+
});
|
|
98
|
+
const wp3Path = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-003-process-meeting', 'work-package.md');
|
|
99
|
+
fs.writeFileSync(wp3Path, fs.readFileSync(wp3Path, 'utf8').replace(
|
|
100
|
+
'- Depends on: (none)',
|
|
101
|
+
'- Depends on: WP-001-foundation',
|
|
102
|
+
));
|
|
103
|
+
|
|
104
|
+
let next = initiativeNext({
|
|
105
|
+
projectRoot: root,
|
|
106
|
+
initiativeId: 'delivery-os-mvp',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(next.next.id).toBe('WP-001-foundation');
|
|
110
|
+
expect(next.readiness.find((item) => item.workPackage.id === 'WP-003-process-meeting').blockers[0]).toContain('Dependency WP-001-foundation');
|
|
111
|
+
|
|
112
|
+
const wp1Path = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-001-foundation', 'work-package.md');
|
|
113
|
+
fs.writeFileSync(wp1Path, fs.readFileSync(wp1Path, 'utf8').replace('Status: pending', 'Status: completed'));
|
|
114
|
+
|
|
115
|
+
next = initiativeNext({
|
|
116
|
+
projectRoot: root,
|
|
117
|
+
initiativeId: 'delivery-os-mvp',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(next.next.id).toBe('WP-003-process-meeting');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('blocks materialization when covered requirements have process-only acceptance', () => {
|
|
124
|
+
const root = makeProject();
|
|
125
|
+
createInitiative({
|
|
126
|
+
projectRoot: root,
|
|
127
|
+
initiativeId: 'delivery-os-mvp',
|
|
128
|
+
title: 'Delivery OS MVP',
|
|
129
|
+
});
|
|
130
|
+
addWorkPackage({
|
|
131
|
+
projectRoot: root,
|
|
132
|
+
initiativeId: 'delivery-os-mvp',
|
|
133
|
+
workPackageId: 'WP-001-foundation',
|
|
134
|
+
title: 'Foundation',
|
|
135
|
+
});
|
|
136
|
+
const wpPath = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-001-foundation', 'work-package.md');
|
|
137
|
+
fs.writeFileSync(wpPath, [
|
|
138
|
+
'# Work Package',
|
|
139
|
+
'',
|
|
140
|
+
'ID: WP-001-foundation',
|
|
141
|
+
'Initiative: delivery-os-mvp',
|
|
142
|
+
'Title: Foundation',
|
|
143
|
+
'Status: pending',
|
|
144
|
+
'Task:',
|
|
145
|
+
'Mode: standard_work_package',
|
|
146
|
+
'',
|
|
147
|
+
'## Goal',
|
|
148
|
+
'',
|
|
149
|
+
'Create foundation.',
|
|
150
|
+
'',
|
|
151
|
+
'## Requirements Coverage',
|
|
152
|
+
'',
|
|
153
|
+
'- REQ-002',
|
|
154
|
+
'',
|
|
155
|
+
'## Dependencies',
|
|
156
|
+
'',
|
|
157
|
+
'- Depends on: (none)',
|
|
158
|
+
'',
|
|
159
|
+
'## Slices',
|
|
160
|
+
'',
|
|
161
|
+
'- Slice 1: Build foundation.',
|
|
162
|
+
'',
|
|
163
|
+
'## Acceptance',
|
|
164
|
+
'',
|
|
165
|
+
'- Work-package task has completed Verify.',
|
|
166
|
+
'- Slice ledger is recorded in execution.md.',
|
|
167
|
+
'- Learning closeout is completed before task closeout.',
|
|
168
|
+
'',
|
|
169
|
+
].join('\n'));
|
|
170
|
+
|
|
171
|
+
const next = initiativeNext({
|
|
172
|
+
projectRoot: root,
|
|
173
|
+
initiativeId: 'delivery-os-mvp',
|
|
174
|
+
materializeTask: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(next.next).toBeNull();
|
|
178
|
+
expect(next.materializedTask).toBeNull();
|
|
179
|
+
expect(next.readiness[0].blockers[0]).toContain('Acceptance is process-only');
|
|
180
|
+
expect(fs.existsSync(path.join(root, 'ops', 'agent-pipeline', 'tasks', 'TASK-001-foundation'))).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
79
183
|
it('indexes source docs and builds a requirements map', () => {
|
|
80
184
|
const root = makeProject();
|
|
81
185
|
const docsDir = path.join(root, 'docs', 'delivery-os');
|
|
@@ -679,6 +783,120 @@ describe('initiative framework', () => {
|
|
|
679
783
|
expect(schemaGate.issue).toContain('meeting_track');
|
|
680
784
|
expect(schemaGate.action).toContain('meetings.raw_speaker_labels');
|
|
681
785
|
});
|
|
786
|
+
|
|
787
|
+
it('blocks synthesis when draft-only proposed_changes storage shape is incomplete', () => {
|
|
788
|
+
const root = makeProject();
|
|
789
|
+
const docsDir = path.join(root, 'docs', 'delivery-os');
|
|
790
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
791
|
+
fs.writeFileSync(path.join(docsDir, '99-feedback-log.md'), [
|
|
792
|
+
'# Feedback',
|
|
793
|
+
'',
|
|
794
|
+
'- Human-approved Variant A: commitments, decisions, risks and financial_event facts are draft-only ProposedChange outputs in Phase 1.',
|
|
795
|
+
'- Drafts need related_person_id, has_conflict and source quote attribution.',
|
|
796
|
+
].join('\n'));
|
|
797
|
+
createInitiative({
|
|
798
|
+
projectRoot: root,
|
|
799
|
+
initiativeId: 'delivery-os-mvp',
|
|
800
|
+
title: 'Delivery OS MVP',
|
|
801
|
+
});
|
|
802
|
+
initiativeIntake({
|
|
803
|
+
projectRoot: root,
|
|
804
|
+
initiativeId: 'delivery-os-mvp',
|
|
805
|
+
docsDir: 'docs/delivery-os',
|
|
806
|
+
phase: 'phase-1',
|
|
807
|
+
});
|
|
808
|
+
const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
|
|
809
|
+
fs.writeFileSync(path.join(initiativeDir, 'requirements-map.md'), [
|
|
810
|
+
'# Requirements Map',
|
|
811
|
+
'',
|
|
812
|
+
'## Requirement Details',
|
|
813
|
+
'',
|
|
814
|
+
'### REQ-005',
|
|
815
|
+
'',
|
|
816
|
+
'- Status: `approved`',
|
|
817
|
+
'- Source: `docs/delivery-os/99-feedback-log.md:1`',
|
|
818
|
+
'- Source hash: `sha`',
|
|
819
|
+
'- Phase: `phase-1`',
|
|
820
|
+
'- Quality: `complete`',
|
|
821
|
+
'- Quality issue: (none)',
|
|
822
|
+
'- Work package: WP-004',
|
|
823
|
+
'- Acceptance: Variant A keeps commitments/decisions/risks as draft ProposedChange records, not applied truth writes.',
|
|
824
|
+
'- Decision: `approve`',
|
|
825
|
+
'- Notes: Human-approved Variant A.',
|
|
826
|
+
'- Requirement: Phase 1 apply writes clients/people/meetings and keeps deferred domains as draft proposed_changes.',
|
|
827
|
+
'',
|
|
828
|
+
].join('\n'));
|
|
829
|
+
|
|
830
|
+
const synthesis = initiativeRequirementsSynthesis({
|
|
831
|
+
projectRoot: root,
|
|
832
|
+
initiativeId: 'delivery-os-mvp',
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const gate = synthesis.gates.find((item) => item.id === 'draft-storage-proposed-changes-shape');
|
|
836
|
+
expect(gate).toMatchObject({
|
|
837
|
+
severity: 'critical',
|
|
838
|
+
blocking: true,
|
|
839
|
+
});
|
|
840
|
+
expect(gate.issue).toContain('entity_type');
|
|
841
|
+
expect(gate.issue).toContain('related_person_id');
|
|
842
|
+
expect(synthesis.rewriteRequired).toBe(true);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('blocks synthesis when audit/entity_history writer model is ambiguous', () => {
|
|
846
|
+
const root = makeProject();
|
|
847
|
+
const docsDir = path.join(root, 'docs', 'delivery-os');
|
|
848
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
849
|
+
fs.writeFileSync(path.join(docsDir, '07-adrs.md'), [
|
|
850
|
+
'# ADR',
|
|
851
|
+
'',
|
|
852
|
+
'- Attribution requires app.current_user_id and entity_history audit before/after records.',
|
|
853
|
+
].join('\n'));
|
|
854
|
+
createInitiative({
|
|
855
|
+
projectRoot: root,
|
|
856
|
+
initiativeId: 'delivery-os-mvp',
|
|
857
|
+
title: 'Delivery OS MVP',
|
|
858
|
+
});
|
|
859
|
+
initiativeIntake({
|
|
860
|
+
projectRoot: root,
|
|
861
|
+
initiativeId: 'delivery-os-mvp',
|
|
862
|
+
docsDir: 'docs/delivery-os',
|
|
863
|
+
phase: 'phase-1',
|
|
864
|
+
});
|
|
865
|
+
const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
|
|
866
|
+
fs.writeFileSync(path.join(initiativeDir, 'requirements-map.md'), [
|
|
867
|
+
'# Requirements Map',
|
|
868
|
+
'',
|
|
869
|
+
'## Requirement Details',
|
|
870
|
+
'',
|
|
871
|
+
'### REQ-002',
|
|
872
|
+
'',
|
|
873
|
+
'- Status: `approved`',
|
|
874
|
+
'- Source: `docs/delivery-os/07-adrs.md:1`',
|
|
875
|
+
'- Source hash: `sha`',
|
|
876
|
+
'- Phase: `phase-1`',
|
|
877
|
+
'- Quality: `complete`',
|
|
878
|
+
'- Quality issue: (none)',
|
|
879
|
+
'- Work package: WP-001',
|
|
880
|
+
'- Acceptance: Schema includes entity_history and app.current_user_id helper pattern.',
|
|
881
|
+
'- Decision: `approve`',
|
|
882
|
+
'- Notes: Audit trigger scope may expand; minimum is entity_history plus helper pattern.',
|
|
883
|
+
'- Requirement: Phase 1 schema preserves attribution and audit history.',
|
|
884
|
+
'',
|
|
885
|
+
].join('\n'));
|
|
886
|
+
|
|
887
|
+
const synthesis = initiativeRequirementsSynthesis({
|
|
888
|
+
projectRoot: root,
|
|
889
|
+
initiativeId: 'delivery-os-mvp',
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
const gate = synthesis.gates.find((item) => item.id === 'audit-writer-model-ambiguous');
|
|
893
|
+
expect(gate).toMatchObject({
|
|
894
|
+
severity: 'needs_decision',
|
|
895
|
+
blocking: true,
|
|
896
|
+
});
|
|
897
|
+
expect(gate.action).toContain('log_entity_change');
|
|
898
|
+
expect(synthesis.rewriteRequired).toBe(true);
|
|
899
|
+
});
|
|
682
900
|
});
|
|
683
901
|
|
|
684
902
|
function makeProject() {
|
|
@@ -228,6 +228,8 @@ export function computeTaskContextInputs(taskDir) {
|
|
|
228
228
|
const qualityGates = analyzePlanQualityGates({
|
|
229
229
|
planContent: taskArtifacts.get('plan.md'),
|
|
230
230
|
risk,
|
|
231
|
+
referencedFiles,
|
|
232
|
+
structuralLines,
|
|
231
233
|
});
|
|
232
234
|
|
|
233
235
|
return {
|
|
@@ -755,7 +757,7 @@ ${missingSignals.length ? missingSignals.map((signal) => `- ${signal}`).join('\n
|
|
|
755
757
|
`;
|
|
756
758
|
}
|
|
757
759
|
|
|
758
|
-
export function analyzePlanQualityGates({ planContent, risk }) {
|
|
760
|
+
export function analyzePlanQualityGates({ planContent, risk, referencedFiles = [], structuralLines = [] }) {
|
|
759
761
|
const sections = parseMarkdownSections(planContent);
|
|
760
762
|
const uiRequired = requiresUiAcceptanceScenarios(risk.riskTriggers);
|
|
761
763
|
const complexityRequired = requiresComplexityBudget(risk.riskTriggers);
|
|
@@ -763,6 +765,12 @@ export function analyzePlanQualityGates({ planContent, risk }) {
|
|
|
763
765
|
const optimizationRequired = requiresOptimizationStrategy(optimizationTier);
|
|
764
766
|
const productionRolloutRequired = requiresProductionRolloutGate(risk.riskTriggers);
|
|
765
767
|
const sourceSyncProviderRequired = requiresSourceSyncProviderGate(risk.riskTriggers);
|
|
768
|
+
const executionMetadata = inspectExecutionMetadata(sections);
|
|
769
|
+
const verificationLadder = inspectVerificationLadder(sections);
|
|
770
|
+
const standardsAlignmentRequired = requiresStandardsAlignment({ referencedFiles, structuralLines });
|
|
771
|
+
const standardsAlignment = inspectStandardsAlignment(sections);
|
|
772
|
+
const migrationApply = inspectMigrationApplyPlan(sections, risk.riskTriggers);
|
|
773
|
+
const auditWriterModel = inspectAuditWriterModel(sections, planContent);
|
|
766
774
|
const uiAcceptance = inspectUiAcceptanceScenarios(sections);
|
|
767
775
|
const complexityBudget = inspectComplexityPerformanceBudget(sections);
|
|
768
776
|
const optimizationStrategy = inspectOptimizationStrategy(sections);
|
|
@@ -770,6 +778,21 @@ export function analyzePlanQualityGates({ planContent, risk }) {
|
|
|
770
778
|
const sourceSyncProvider = inspectSourceSyncProviderGate(sections);
|
|
771
779
|
const missingSignals = [];
|
|
772
780
|
|
|
781
|
+
if (!executionMetadata.present) {
|
|
782
|
+
missingSignals.push('Plan must include `## Risk tier and execution budget` with explicit risk tier, speed mode, execution target and budget/stop rule.');
|
|
783
|
+
}
|
|
784
|
+
if (!verificationLadder.present) {
|
|
785
|
+
missingSignals.push('Plan must include a `Verification ladder` with micro-verify, slice-verify and external Verify decision.');
|
|
786
|
+
}
|
|
787
|
+
if (standardsAlignmentRequired && !standardsAlignment.present) {
|
|
788
|
+
missingSignals.push('Standards file/reference detected but `## Global Standards Alignment` / `## Standards Alignment` is missing or incomplete.');
|
|
789
|
+
}
|
|
790
|
+
if (migrationApply.required && !migrationApply.present) {
|
|
791
|
+
missingSignals.push('Schema/migration work requires a real apply path on a disposable/scratch database before closeout; static SQL review alone is not enough.');
|
|
792
|
+
}
|
|
793
|
+
if (auditWriterModel.required && !auditWriterModel.present) {
|
|
794
|
+
missingSignals.push('Audit/entity_history requirement must declare the writer model: functional DB trigger versus explicit application-code writer, not an ambiguous table-only audit.');
|
|
795
|
+
}
|
|
773
796
|
if (uiRequired && !uiAcceptance.present) {
|
|
774
797
|
missingSignals.push('UI-visible risk detected but `## UI Acceptance Scenarios` is missing or incomplete.');
|
|
775
798
|
}
|
|
@@ -782,6 +805,9 @@ export function analyzePlanQualityGates({ planContent, risk }) {
|
|
|
782
805
|
if (optimizationRequired && !optimizationStrategy.present) {
|
|
783
806
|
missingSignals.push(`Optimization tier ${optimizationTier} detected but \`## Optimization Strategy\` is missing or incomplete.`);
|
|
784
807
|
}
|
|
808
|
+
if (optimizationTier === 'O3' && optimizationStrategy.present && !optimizationStrategy.hasMeasurementPath) {
|
|
809
|
+
missingSignals.push('Optimization tier O3 requires a representative measurement path or explicit blocked/human-owned fallback.');
|
|
810
|
+
}
|
|
785
811
|
if (productionRolloutRequired && !productionRollout.present) {
|
|
786
812
|
missingSignals.push('Production rollout risk detected but `## Production Rollout Gate` is missing or incomplete.');
|
|
787
813
|
}
|
|
@@ -790,6 +816,12 @@ export function analyzePlanQualityGates({ planContent, risk }) {
|
|
|
790
816
|
}
|
|
791
817
|
|
|
792
818
|
return {
|
|
819
|
+
executionMetadata,
|
|
820
|
+
verificationLadder,
|
|
821
|
+
standardsAlignmentRequired,
|
|
822
|
+
standardsAlignment,
|
|
823
|
+
migrationApply,
|
|
824
|
+
auditWriterModel,
|
|
793
825
|
uiRequired,
|
|
794
826
|
uiAcceptance,
|
|
795
827
|
complexityRequired,
|
|
@@ -805,6 +837,173 @@ export function analyzePlanQualityGates({ planContent, risk }) {
|
|
|
805
837
|
};
|
|
806
838
|
}
|
|
807
839
|
|
|
840
|
+
export function inspectExecutionMetadata(sections) {
|
|
841
|
+
const body = readCanonicalSection(sections, [
|
|
842
|
+
'risk tier and execution budget',
|
|
843
|
+
'risk tier',
|
|
844
|
+
'execution budget',
|
|
845
|
+
]);
|
|
846
|
+
if (!body) {
|
|
847
|
+
return {
|
|
848
|
+
present: false,
|
|
849
|
+
hasRiskTier: false,
|
|
850
|
+
hasSpeedMode: false,
|
|
851
|
+
hasExecutionTarget: false,
|
|
852
|
+
hasBudget: false,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const normalized = body.toLowerCase();
|
|
857
|
+
const targetValue = readLabeledValue(body, ['approved execution target', 'execution target', 'target']);
|
|
858
|
+
const budgetValue = firstNonEmptyValue([
|
|
859
|
+
readLabeledValue(body, ['execution budget', 'budget', 'optimizer budget / stop rule']),
|
|
860
|
+
readLabeledValue(body, ['fast-loop allowed inside this slice', 'fast-loop allowed']),
|
|
861
|
+
readLabeledValue(body, ['requires return to plan/check if', 'requires return']),
|
|
862
|
+
]);
|
|
863
|
+
const result = {
|
|
864
|
+
present: true,
|
|
865
|
+
hasRiskTier: /risk tier\s*:\s*`?\s*R[0-5]\b/i.test(body) && !/risk tier\s*:\s*`?\s*R0\s*\|/i.test(body),
|
|
866
|
+
hasSpeedMode: /speed mode\s*:\s*`?\s*(fast|standard|strict)\b/i.test(body) && !/speed mode\s*:\s*`?\s*fast\s*\|/i.test(body),
|
|
867
|
+
hasExecutionTarget: Boolean(targetValue && !/^(tbd|todo|n\/a|none|-)?$/i.test(targetValue.trim())),
|
|
868
|
+
hasBudget: Boolean(budgetValue && !/^(tbd|todo|n\/a|none|-)?$/i.test(budgetValue.trim())) || /(?:budget|stop rule|лимит|бюджет)\s*:\s*\S/i.test(normalized),
|
|
869
|
+
};
|
|
870
|
+
result.complete = result.hasRiskTier && result.hasSpeedMode && result.hasExecutionTarget && result.hasBudget;
|
|
871
|
+
result.present = result.complete;
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function inspectVerificationLadder(sections) {
|
|
876
|
+
const body = [
|
|
877
|
+
readCanonicalSection(sections, ['verification ladder']),
|
|
878
|
+
readCanonicalSection(sections, ['план проверки', 'verification plan', 'verification and replay plan']),
|
|
879
|
+
].filter(Boolean).join('\n');
|
|
880
|
+
if (!body) {
|
|
881
|
+
return {
|
|
882
|
+
present: false,
|
|
883
|
+
hasMicroVerify: false,
|
|
884
|
+
hasSliceVerify: false,
|
|
885
|
+
hasExternalVerifyDecision: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const normalized = body.toLowerCase();
|
|
890
|
+
const result = {
|
|
891
|
+
present: true,
|
|
892
|
+
hasMicroVerify: /micro-?verify|micro verify|микро/.test(normalized),
|
|
893
|
+
hasSliceVerify: /slice-?verify|slice verify|слайс/.test(normalized),
|
|
894
|
+
hasExternalVerifyDecision: /external verify|required before closeout|yes\s*\|\s*no|external_cli|internal_supervisor|внешн/.test(normalized),
|
|
895
|
+
};
|
|
896
|
+
result.complete = result.hasMicroVerify && result.hasSliceVerify && result.hasExternalVerifyDecision;
|
|
897
|
+
result.present = result.complete;
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function requiresStandardsAlignment({ referencedFiles = [], structuralLines = [] } = {}) {
|
|
902
|
+
const text = [...referencedFiles, ...structuralLines].join('\n').toLowerCase();
|
|
903
|
+
return /(^|\/|\.)(claude|standards|standard|rules|conventions)(\.md|\.mdc|\/|$)/.test(text)
|
|
904
|
+
|| /\b(standards|conventions|coding rules|global rules|claude\.md)\b/.test(text);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export function inspectStandardsAlignment(sections) {
|
|
908
|
+
const body = readCanonicalSection(sections, [
|
|
909
|
+
'global standards alignment',
|
|
910
|
+
'standards alignment',
|
|
911
|
+
'standards',
|
|
912
|
+
]);
|
|
913
|
+
if (!body) {
|
|
914
|
+
return {
|
|
915
|
+
present: false,
|
|
916
|
+
hasLocalInterpretation: false,
|
|
917
|
+
hasValidation: false,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const normalized = body.toLowerCase();
|
|
922
|
+
const result = {
|
|
923
|
+
present: true,
|
|
924
|
+
hasLocalInterpretation: /local interpretation|applied here|applies|интерпретац|применим|учитываем|следуем/.test(normalized),
|
|
925
|
+
hasValidation: /validate|verification|check|test|lint|провер/.test(normalized),
|
|
926
|
+
};
|
|
927
|
+
result.complete = result.hasLocalInterpretation && result.hasValidation;
|
|
928
|
+
result.present = result.complete;
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function inspectMigrationApplyPlan(sections, riskTriggers = []) {
|
|
933
|
+
const required = riskTriggers.includes('prisma-schema');
|
|
934
|
+
const body = [
|
|
935
|
+
readCanonicalSection(sections, ['production rollout gate', 'production rollout', 'rollout gate', 'deployment gate']),
|
|
936
|
+
readCanonicalSection(sections, ['план проверки', 'verification plan', 'verification and replay plan']),
|
|
937
|
+
readCanonicalSection(sections, ['risk tier and execution budget']),
|
|
938
|
+
].filter(Boolean).join('\n');
|
|
939
|
+
if (!required) {
|
|
940
|
+
return {
|
|
941
|
+
required: false,
|
|
942
|
+
present: true,
|
|
943
|
+
hasApplyCommand: false,
|
|
944
|
+
hasDisposableTarget: false,
|
|
945
|
+
rejectsStaticOnly: false,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
if (!body) {
|
|
949
|
+
return {
|
|
950
|
+
required,
|
|
951
|
+
present: false,
|
|
952
|
+
hasApplyCommand: false,
|
|
953
|
+
hasDisposableTarget: false,
|
|
954
|
+
rejectsStaticOnly: false,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const normalized = body.toLowerCase();
|
|
959
|
+
const result = {
|
|
960
|
+
required,
|
|
961
|
+
present: true,
|
|
962
|
+
hasApplyCommand: /\b(apply|migrate|psql|db push|migration)\b/.test(normalized),
|
|
963
|
+
hasDisposableTarget: /\b(disposable|scratch|throwaway|temporary|temp|ephemeral|test database|local postgres|railway postgres|staging db|однораз|временн|тестов)\b/.test(normalized),
|
|
964
|
+
rejectsStaticOnly: /static(?: sql)? review alone is not enough|not static[- ]only|статическ.{0,40}недостаточно|blocked evidence.{0,80}not accepted/i.test(body),
|
|
965
|
+
};
|
|
966
|
+
result.complete = result.hasApplyCommand && result.hasDisposableTarget;
|
|
967
|
+
result.present = result.complete;
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function inspectAuditWriterModel(sections, planContent = '') {
|
|
972
|
+
const text = [
|
|
973
|
+
planContent,
|
|
974
|
+
readCanonicalSection(sections, ['production rollout gate', 'production rollout']),
|
|
975
|
+
readCanonicalSection(sections, ['шаги реализации', 'implementation steps']),
|
|
976
|
+
readCanonicalSection(sections, ['риски и открытые вопросы', 'known risks']),
|
|
977
|
+
].filter(Boolean).join('\n');
|
|
978
|
+
const required = /(entity_history|audit|before\/after|before and after|log_entity_change|current_setting\('app\.current_user_id'\)|app\.current_user_id|attribution)/i.test(text);
|
|
979
|
+
if (!required) {
|
|
980
|
+
return {
|
|
981
|
+
required: false,
|
|
982
|
+
present: true,
|
|
983
|
+
writerModel: null,
|
|
984
|
+
ambiguous: false,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const triggerModel = /(trigger|log_entity_change|before\/after|before and after|current_setting\('app\.current_user_id'\)|навеш|триггер)/i.test(text);
|
|
989
|
+
const appModel = /(application[- ]code writer|app(?:lication)? writes|apply[- ]code writes|manual writer|writes entity_history manually|пишет entity_history|прикладн)/i.test(text);
|
|
990
|
+
const tableOnlyAmbiguity = /(table only|only table|таблиц[ауы].{0,80}без триггер|entity_history.{0,120}(without|no) trigger|helper pattern only|minimum is entity_history plus helper pattern)/i.test(text);
|
|
991
|
+
const eitherOrAmbiguity = /(trigger|триггер).{0,80}\b(or|или)\b.{0,80}(application|app|manual|код|вручн)/i.test(text)
|
|
992
|
+
|| /(application|app|manual|код|вручн).{0,80}\b(or|или)\b.{0,80}(trigger|триггер)/i.test(text);
|
|
993
|
+
const ambiguous = tableOnlyAmbiguity || eitherOrAmbiguity || (!triggerModel && !appModel);
|
|
994
|
+
const writerModel = triggerModel && !eitherOrAmbiguity
|
|
995
|
+
? 'db_trigger'
|
|
996
|
+
: appModel && !eitherOrAmbiguity
|
|
997
|
+
? 'application_code'
|
|
998
|
+
: null;
|
|
999
|
+
return {
|
|
1000
|
+
required,
|
|
1001
|
+
present: Boolean(writerModel && !ambiguous),
|
|
1002
|
+
writerModel,
|
|
1003
|
+
ambiguous,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
808
1007
|
export function requiresUiAcceptanceScenarios(riskTriggers = []) {
|
|
809
1008
|
return riskTriggers.includes('panel-ui') || riskTriggers.includes('ui-visible-api');
|
|
810
1009
|
}
|
|
@@ -953,13 +1152,16 @@ export function inspectOptimizationStrategy(sections) {
|
|
|
953
1152
|
hasApproach: /strategy|approach|chosen|batch|map|set|index|query|window|memo|cache|подход|стратег/.test(normalized),
|
|
954
1153
|
hasAntiPatterns: /n\+1|repeated scan|nested|o\(n\^2\)|unbounded|full scan|render|anti-pattern|антипаттерн/.test(normalized),
|
|
955
1154
|
hasBudget: /budget|stop rule|stop-rule|minutes|минут|measurement|timing|benchmark|explain|defer|бюджет|лимит/.test(normalized),
|
|
1155
|
+
hasMeasurementPath: /measurement|timing|benchmark|profile|profiling|explain|representative|smoke metric|human-owned fallback|blocked fallback|измер|замер|метрик/.test(normalized),
|
|
956
1156
|
};
|
|
1157
|
+
const tierRequiresMeasurement = result.tier === 'O3';
|
|
957
1158
|
result.complete = Boolean(result.tier)
|
|
958
1159
|
&& result.hasHotPaths
|
|
959
1160
|
&& result.hasDataSize
|
|
960
1161
|
&& result.hasApproach
|
|
961
1162
|
&& result.hasAntiPatterns
|
|
962
|
-
&& result.hasBudget
|
|
1163
|
+
&& result.hasBudget
|
|
1164
|
+
&& (!tierRequiresMeasurement || result.hasMeasurementPath);
|
|
963
1165
|
result.present = result.complete;
|
|
964
1166
|
return result;
|
|
965
1167
|
}
|
|
@@ -1034,6 +1236,21 @@ function readCanonicalSection(sections, names) {
|
|
|
1034
1236
|
return '';
|
|
1035
1237
|
}
|
|
1036
1238
|
|
|
1239
|
+
function readLabeledValue(body, labels) {
|
|
1240
|
+
for (const label of labels) {
|
|
1241
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1242
|
+
const match = new RegExp(`${escaped}\\s*:\\s*([^\\n]+)`, 'i').exec(body);
|
|
1243
|
+
if (match) {
|
|
1244
|
+
return match[1].replace(/^[-\s`]*/, '').replace(/`+$/, '').trim();
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return '';
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function firstNonEmptyValue(values) {
|
|
1251
|
+
return values.find((value) => typeof value === 'string' && value.trim()) || '';
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1037
1254
|
export function buildCheckerContextPack({
|
|
1038
1255
|
taskId,
|
|
1039
1256
|
risk,
|
|
@@ -1313,6 +1530,28 @@ export function validateExecutionEvidenceForPlan({ planContent, executionContent
|
|
|
1313
1530
|
}
|
|
1314
1531
|
}
|
|
1315
1532
|
|
|
1533
|
+
if (planRequiresRealMigrationApply(planContent)) {
|
|
1534
|
+
const evidence = readAnySection(executionSections, [
|
|
1535
|
+
'migration apply evidence',
|
|
1536
|
+
'schema migration evidence',
|
|
1537
|
+
'production rollout evidence',
|
|
1538
|
+
'rollout evidence',
|
|
1539
|
+
'preflight evidence',
|
|
1540
|
+
'micro-verify evidence',
|
|
1541
|
+
]);
|
|
1542
|
+
if (!evidence) {
|
|
1543
|
+
errors.push({
|
|
1544
|
+
category: 'unrun_required_check',
|
|
1545
|
+
message: 'Schema/migration plan requires real disposable/scratch DB apply evidence before Verify/Human Gate.',
|
|
1546
|
+
});
|
|
1547
|
+
} else if (!/(apply|migrate|psql|migration|db push)/i.test(evidence) || !/(pass|passed|success|ok|applied|успеш)/i.test(evidence) || !/(scratch|throwaway|disposable|temporary|temp|ephemeral|test database|local postgres|railway postgres|однораз|временн|тестов)/i.test(evidence)) {
|
|
1548
|
+
errors.push({
|
|
1549
|
+
category: 'insufficient_evidence',
|
|
1550
|
+
message: 'Migration Apply Evidence must show a successful apply/migrate/psql run against a disposable/scratch database target.',
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1316
1555
|
if (hasAnySection(planSections, ['source sync / provider gate', 'source sync provider gate', 'source sync gate', 'provider gate'])) {
|
|
1317
1556
|
const evidence = readAnySection(executionSections, ['source sync / provider evidence', 'source sync evidence', 'provider evidence']);
|
|
1318
1557
|
if (!evidence) {
|
|
@@ -1331,6 +1570,16 @@ export function validateExecutionEvidenceForPlan({ planContent, executionContent
|
|
|
1331
1570
|
return errors;
|
|
1332
1571
|
}
|
|
1333
1572
|
|
|
1573
|
+
function planRequiresRealMigrationApply(planContent) {
|
|
1574
|
+
const sections = parseMarkdownSections(planContent || '');
|
|
1575
|
+
const planText = planContent || '';
|
|
1576
|
+
const hasMigration = /(schema\.prisma|prisma\/migrations|migration|migrations?|create table|alter table|plain sql|DDL|схем|миграц)/i.test(planText);
|
|
1577
|
+
if (!hasMigration) {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
return inspectMigrationApplyPlan(sections, ['prisma-schema']).present;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1334
1583
|
function hasAnySection(sections, names) {
|
|
1335
1584
|
return Boolean(readAnySection(sections, names));
|
|
1336
1585
|
}
|
|
@@ -13,12 +13,18 @@ import {
|
|
|
13
13
|
riskRootWarnings,
|
|
14
14
|
selectRelevantPlaybookNames,
|
|
15
15
|
inspectComplexityPerformanceBudget,
|
|
16
|
+
inspectExecutionMetadata,
|
|
17
|
+
inspectAuditWriterModel,
|
|
18
|
+
inspectMigrationApplyPlan,
|
|
16
19
|
inspectOptimizationStrategy,
|
|
17
20
|
inspectProductionRolloutGate,
|
|
18
21
|
inspectSourceSyncProviderGate,
|
|
22
|
+
inspectStandardsAlignment,
|
|
19
23
|
inspectUiAcceptanceScenarios,
|
|
24
|
+
inspectVerificationLadder,
|
|
20
25
|
parseMarkdownSections,
|
|
21
26
|
requiresOptimizationStrategy,
|
|
27
|
+
requiresStandardsAlignment,
|
|
22
28
|
validateExecutionEvidenceForPlan,
|
|
23
29
|
} from './check-context-utils.mjs';
|
|
24
30
|
|
|
@@ -49,6 +55,176 @@ describe('agent pipeline quality gates', () => {
|
|
|
49
55
|
expect(result.missingSignals).toContain('UI-visible risk detected but `## UI Acceptance Scenarios` is missing or incomplete.');
|
|
50
56
|
});
|
|
51
57
|
|
|
58
|
+
it('blocks external check locally when execution metadata is placeholder-only', () => {
|
|
59
|
+
const plan = [
|
|
60
|
+
'# Plan',
|
|
61
|
+
'',
|
|
62
|
+
'## Risk tier and execution budget',
|
|
63
|
+
'',
|
|
64
|
+
'- Risk tier: `R0 | R1 | R2 | R3 | R4 | R5`',
|
|
65
|
+
'- Speed mode: `Fast | Standard | Strict`',
|
|
66
|
+
'- Approved execution target:',
|
|
67
|
+
'- Fast-loop allowed inside this slice:',
|
|
68
|
+
'- Requires return to Plan/Check if:',
|
|
69
|
+
].join('\n');
|
|
70
|
+
|
|
71
|
+
const result = analyzePlanQualityGates({
|
|
72
|
+
planContent: plan,
|
|
73
|
+
risk: {
|
|
74
|
+
riskProfile: 'low',
|
|
75
|
+
riskTriggers: ['docs-only'],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.executionMetadata.present).toBe(false);
|
|
80
|
+
expect(result.missingSignals).toContain('Plan must include `## Risk tier and execution budget` with explicit risk tier, speed mode, execution target and budget/stop rule.');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('accepts explicit execution metadata and verification ladder', () => {
|
|
84
|
+
const sections = parseMarkdownSections([
|
|
85
|
+
'# Plan',
|
|
86
|
+
'',
|
|
87
|
+
'## Risk tier and execution budget',
|
|
88
|
+
'',
|
|
89
|
+
'- Risk tier: `R2`',
|
|
90
|
+
'- Speed mode: `Standard`',
|
|
91
|
+
'- Approved execution target: local schema files and API DTOs only.',
|
|
92
|
+
'- Fast-loop allowed inside this slice: yes, until migration scope changes.',
|
|
93
|
+
'- Requires return to Plan/Check if: destructive migration or production data write appears.',
|
|
94
|
+
'',
|
|
95
|
+
'## План проверки',
|
|
96
|
+
'',
|
|
97
|
+
'### Verification ladder',
|
|
98
|
+
'',
|
|
99
|
+
'- Micro-verify during Execute: node --check.',
|
|
100
|
+
'- Slice-verify before completion: tests.',
|
|
101
|
+
'- External Verify required before closeout: no, internal_supervisor is enough.',
|
|
102
|
+
].join('\n'));
|
|
103
|
+
|
|
104
|
+
expect(inspectExecutionMetadata(sections).present).toBe(true);
|
|
105
|
+
expect(inspectVerificationLadder(sections).present).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('requires standards alignment only when standards references are present', () => {
|
|
109
|
+
expect(requiresStandardsAlignment({
|
|
110
|
+
referencedFiles: ['../.claude/CLAUDE.MD'],
|
|
111
|
+
structuralLines: [],
|
|
112
|
+
})).toBe(true);
|
|
113
|
+
expect(requiresStandardsAlignment({
|
|
114
|
+
referencedFiles: ['docs/01-overview.md'],
|
|
115
|
+
structuralLines: [],
|
|
116
|
+
})).toBe(false);
|
|
117
|
+
|
|
118
|
+
const missing = analyzePlanQualityGates({
|
|
119
|
+
planContent: '# Plan\n',
|
|
120
|
+
risk: {
|
|
121
|
+
riskProfile: 'low',
|
|
122
|
+
riskTriggers: ['docs-only'],
|
|
123
|
+
},
|
|
124
|
+
referencedFiles: ['../.claude/CLAUDE.MD'],
|
|
125
|
+
});
|
|
126
|
+
expect(missing.missingSignals).toContain('Standards file/reference detected but `## Global Standards Alignment` / `## Standards Alignment` is missing or incomplete.');
|
|
127
|
+
|
|
128
|
+
const sections = parseMarkdownSections([
|
|
129
|
+
'# Plan',
|
|
130
|
+
'',
|
|
131
|
+
'## Global Standards Alignment',
|
|
132
|
+
'',
|
|
133
|
+
'- Local interpretation: apply repo naming and validation rules to this task.',
|
|
134
|
+
'- Validation: run lint/test commands from the plan.',
|
|
135
|
+
].join('\n'));
|
|
136
|
+
expect(inspectStandardsAlignment(sections).present).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('requires a real disposable migration apply path for schema work before external check', () => {
|
|
140
|
+
const result = analyzePlanQualityGates({
|
|
141
|
+
planContent: [
|
|
142
|
+
'# Plan',
|
|
143
|
+
'',
|
|
144
|
+
'## Risk tier and execution budget',
|
|
145
|
+
'',
|
|
146
|
+
'- Risk tier: `R3`',
|
|
147
|
+
'- Speed mode: `Standard`',
|
|
148
|
+
'- Approved execution target: plain SQL migration.',
|
|
149
|
+
'- Requires return to Plan/Check if: database apply cannot run.',
|
|
150
|
+
'',
|
|
151
|
+
'## План проверки',
|
|
152
|
+
'',
|
|
153
|
+
'### Verification ladder',
|
|
154
|
+
'',
|
|
155
|
+
'- Micro-verify during Execute: static SQL review.',
|
|
156
|
+
'- Slice-verify before completion: blocked if Postgres unavailable.',
|
|
157
|
+
'- External Verify required before closeout: no.',
|
|
158
|
+
'',
|
|
159
|
+
'## Production Rollout Gate',
|
|
160
|
+
'',
|
|
161
|
+
'- Impact / blast radius: local schema migration only.',
|
|
162
|
+
'- Environment / deploy variables: none.',
|
|
163
|
+
'- Rollback / disable path: revert migration file.',
|
|
164
|
+
'- Post-deploy evidence: static SQL review if Postgres unavailable.',
|
|
165
|
+
].join('\n'),
|
|
166
|
+
risk: {
|
|
167
|
+
riskProfile: 'high',
|
|
168
|
+
riskTriggers: ['prisma-schema'],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.migrationApply.required).toBe(true);
|
|
173
|
+
expect(result.migrationApply.present).toBe(false);
|
|
174
|
+
expect(result.missingSignals).toContain('Schema/migration work requires a real apply path on a disposable/scratch database before closeout; static SQL review alone is not enough.');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('accepts migration apply plan with disposable database target', () => {
|
|
178
|
+
const sections = parseMarkdownSections([
|
|
179
|
+
'# Plan',
|
|
180
|
+
'',
|
|
181
|
+
'## Production Rollout Gate',
|
|
182
|
+
'',
|
|
183
|
+
'- Impact / blast radius: local schema migration only.',
|
|
184
|
+
'- Environment / deploy variables: temporary Railway Postgres scratch database.',
|
|
185
|
+
'- Rollback / disable path: drop scratch database and revert migration file.',
|
|
186
|
+
'- Post-deploy evidence: apply plain SQL migration with psql against throwaway Postgres; static SQL review alone is not enough.',
|
|
187
|
+
].join('\n'));
|
|
188
|
+
|
|
189
|
+
const result = inspectMigrationApplyPlan(sections, ['prisma-schema']);
|
|
190
|
+
|
|
191
|
+
expect(result.present).toBe(true);
|
|
192
|
+
expect(result.hasApplyCommand).toBe(true);
|
|
193
|
+
expect(result.hasDisposableTarget).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('requires audit writer model when entity_history audit is in scope', () => {
|
|
197
|
+
const sections = parseMarkdownSections([
|
|
198
|
+
'# Plan',
|
|
199
|
+
'',
|
|
200
|
+
'## Риски и открытые вопросы',
|
|
201
|
+
'',
|
|
202
|
+
'- Full audit triggers may expand scope; minimum is entity_history plus helper pattern.',
|
|
203
|
+
].join('\n'));
|
|
204
|
+
|
|
205
|
+
const result = inspectAuditWriterModel(sections, Array.from(sections.values()).join('\n'));
|
|
206
|
+
|
|
207
|
+
expect(result.required).toBe(true);
|
|
208
|
+
expect(result.present).toBe(false);
|
|
209
|
+
expect(result.ambiguous).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('accepts explicit DB trigger audit writer model', () => {
|
|
213
|
+
const sections = parseMarkdownSections([
|
|
214
|
+
'# Plan',
|
|
215
|
+
'',
|
|
216
|
+
'## Шаги реализации',
|
|
217
|
+
'',
|
|
218
|
+
'- Create entity_history.',
|
|
219
|
+
"- Create log_entity_change trigger function using current_setting('app.current_user_id') and attach triggers to core tables for before/after audit.",
|
|
220
|
+
].join('\n'));
|
|
221
|
+
|
|
222
|
+
const result = inspectAuditWriterModel(sections, Array.from(sections.values()).join('\n'));
|
|
223
|
+
|
|
224
|
+
expect(result.present).toBe(true);
|
|
225
|
+
expect(result.writerModel).toBe('db_trigger');
|
|
226
|
+
});
|
|
227
|
+
|
|
52
228
|
it('accepts UI acceptance scenarios with intent, steps, expected visible result and must-catch regressions', () => {
|
|
53
229
|
const sections = parseMarkdownSections([
|
|
54
230
|
'# Plan',
|
|
@@ -199,6 +375,54 @@ describe('agent pipeline quality gates', () => {
|
|
|
199
375
|
expect(result.hasApproach).toBe(true);
|
|
200
376
|
expect(result.hasAntiPatterns).toBe(true);
|
|
201
377
|
expect(result.hasBudget).toBe(true);
|
|
378
|
+
expect(result.hasMeasurementPath).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('requires a measurement path for O3 optimization strategy before external check', () => {
|
|
382
|
+
const result = analyzePlanQualityGates({
|
|
383
|
+
planContent: [
|
|
384
|
+
'# Plan',
|
|
385
|
+
'',
|
|
386
|
+
'## Risk tier and execution budget',
|
|
387
|
+
'',
|
|
388
|
+
'- Risk tier: `R3`',
|
|
389
|
+
'- Speed mode: `Standard`',
|
|
390
|
+
'- Approved execution target: worker hot path.',
|
|
391
|
+
'- Requires return to Plan/Check if: budget changes.',
|
|
392
|
+
'',
|
|
393
|
+
'## План проверки',
|
|
394
|
+
'',
|
|
395
|
+
'### Verification ladder',
|
|
396
|
+
'',
|
|
397
|
+
'- Micro-verify during Execute: unit test.',
|
|
398
|
+
'- Slice-verify before completion: smoke test.',
|
|
399
|
+
'- External Verify required before closeout: no.',
|
|
400
|
+
'',
|
|
401
|
+
'## Complexity / Performance Budget',
|
|
402
|
+
'',
|
|
403
|
+
'- Hot paths: worker materializer.',
|
|
404
|
+
'- Expected data size / row counts: 150k rows.',
|
|
405
|
+
'- Complexity risks: N+1 queries.',
|
|
406
|
+
'- Budget / stop rule: < 3 seconds.',
|
|
407
|
+
'',
|
|
408
|
+
'## Optimization Strategy',
|
|
409
|
+
'',
|
|
410
|
+
'- Optimization tier: O3 measured review.',
|
|
411
|
+
'- Hot paths: worker materializer.',
|
|
412
|
+
'- Expected data size / row counts: 150k rows.',
|
|
413
|
+
'- Chosen efficient approach: batch query and indexed lookup.',
|
|
414
|
+
'- Anti-patterns avoided: N+1 queries and repeated scans.',
|
|
415
|
+
'- Optimizer budget / stop rule: one focused review; defer speculative ideas.',
|
|
416
|
+
].join('\n'),
|
|
417
|
+
risk: {
|
|
418
|
+
riskProfile: 'high',
|
|
419
|
+
riskTriggers: ['production-runtime'],
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
expect(result.optimizationTier).toBe('O3');
|
|
424
|
+
expect(result.optimizationStrategy.present).toBe(false);
|
|
425
|
+
expect(result.missingSignals).toContain('Optimization tier O3 detected but `## Optimization Strategy` is missing or incomplete.');
|
|
202
426
|
});
|
|
203
427
|
|
|
204
428
|
it('builds a checker context pack with exact quality-gate questions', () => {
|
|
@@ -431,6 +655,42 @@ describe('agent pipeline quality gates', () => {
|
|
|
431
655
|
expect(issues).toEqual([]);
|
|
432
656
|
});
|
|
433
657
|
|
|
658
|
+
it('pre-verify evidence gate blocks migration plans without successful scratch DB apply evidence', () => {
|
|
659
|
+
const plan = [
|
|
660
|
+
'# Plan',
|
|
661
|
+
'',
|
|
662
|
+
'## Production Rollout Gate',
|
|
663
|
+
'',
|
|
664
|
+
'- Impact / blast radius: local schema migration only.',
|
|
665
|
+
'- Environment / deploy variables: temporary Railway Postgres scratch database.',
|
|
666
|
+
'- Rollback / disable path: drop scratch database and revert migration file.',
|
|
667
|
+
'- Post-deploy evidence: apply plain SQL migration with psql against throwaway Postgres; static SQL review alone is not enough.',
|
|
668
|
+
'',
|
|
669
|
+
'## Шаги реализации',
|
|
670
|
+
'',
|
|
671
|
+
'- Create plain SQL migration with CREATE TABLE statements.',
|
|
672
|
+
].join('\n');
|
|
673
|
+
const execution = [
|
|
674
|
+
'# Execution',
|
|
675
|
+
'',
|
|
676
|
+
'## Production Rollout Evidence',
|
|
677
|
+
'',
|
|
678
|
+
'| Check | Result | Evidence | Notes |',
|
|
679
|
+
'| --- | --- | --- | --- |',
|
|
680
|
+
'| migration | blocked | Postgres unavailable; static SQL review only | n/a |',
|
|
681
|
+
].join('\n');
|
|
682
|
+
|
|
683
|
+
const issues = validateExecutionEvidenceForPlan({
|
|
684
|
+
planContent: plan,
|
|
685
|
+
executionContent: execution,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
expect(issues).toContainEqual({
|
|
689
|
+
category: 'insufficient_evidence',
|
|
690
|
+
message: 'Migration Apply Evidence must show a successful apply/migrate/psql run against a disposable/scratch database target.',
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
434
694
|
it('requires production rollout gate for production runtime triggers', () => {
|
|
435
695
|
const result = analyzePlanQualityGates({
|
|
436
696
|
planContent: '# Plan\n\n## Затронутые модули и файлы\n\n- deploy runtime env variable for worker',
|
|
@@ -26,7 +26,18 @@ describe('task manifest utilities', () => {
|
|
|
26
26
|
'',
|
|
27
27
|
'## Risk tier and execution budget',
|
|
28
28
|
'',
|
|
29
|
+
'- Risk tier: `R1`',
|
|
29
30
|
'- Speed mode: `Fast`',
|
|
31
|
+
'- Approved execution target: docs/example.md only.',
|
|
32
|
+
'- Requires return to Plan/Check if: code changes are needed.',
|
|
33
|
+
'',
|
|
34
|
+
'## План проверки',
|
|
35
|
+
'',
|
|
36
|
+
'### Verification ladder',
|
|
37
|
+
'',
|
|
38
|
+
'- Micro-verify during Execute: markdown review.',
|
|
39
|
+
'- Slice-verify before completion: self-test.',
|
|
40
|
+
'- External Verify required before closeout: no.',
|
|
30
41
|
'',
|
|
31
42
|
'## Затронутые модули и файлы',
|
|
32
43
|
'',
|
|
@@ -94,7 +105,18 @@ describe('task manifest utilities', () => {
|
|
|
94
105
|
'',
|
|
95
106
|
'## Risk tier and execution budget',
|
|
96
107
|
'',
|
|
108
|
+
'- Risk tier: `R1`',
|
|
97
109
|
'- Speed mode: `Fast`',
|
|
110
|
+
'- Approved execution target: docs/example.md only.',
|
|
111
|
+
'- Requires return to Plan/Check if: code changes are needed.',
|
|
112
|
+
'',
|
|
113
|
+
'## План проверки',
|
|
114
|
+
'',
|
|
115
|
+
'### Verification ladder',
|
|
116
|
+
'',
|
|
117
|
+
'- Micro-verify during Execute: markdown review.',
|
|
118
|
+
'- Slice-verify before completion: self-test.',
|
|
119
|
+
'- External Verify required before closeout: no.',
|
|
98
120
|
'',
|
|
99
121
|
'## Затронутые модули и файлы',
|
|
100
122
|
'',
|
package/bin/run-check.mjs
CHANGED
|
@@ -66,6 +66,13 @@ async function runMain() {
|
|
|
66
66
|
const noCache = getFlag(args, 'no-cache', false) === true;
|
|
67
67
|
const checkerConfig = resolveCheckerConfig(args);
|
|
68
68
|
const runStartedAt = new Date();
|
|
69
|
+
appendCheckTimeline(taskDir, {
|
|
70
|
+
event: 'check_started',
|
|
71
|
+
provider: checkerConfig.provider,
|
|
72
|
+
model: checkerConfig.model,
|
|
73
|
+
noCache,
|
|
74
|
+
dryRun,
|
|
75
|
+
});
|
|
69
76
|
|
|
70
77
|
let checkContext = ensureFreshCheckContext(taskDir, taskId);
|
|
71
78
|
const deterministicPrecheck = syncManifestAndStatusBeforeCheck({ taskDir, taskId, checkContext });
|
|
@@ -79,6 +86,12 @@ async function runMain() {
|
|
|
79
86
|
issues: deterministicPrecheck.issues,
|
|
80
87
|
startedAt: runStartedAt,
|
|
81
88
|
});
|
|
89
|
+
appendCheckTimeline(taskDir, {
|
|
90
|
+
event: 'deterministic_precheck_blocked',
|
|
91
|
+
verdict: 'return_to_plan',
|
|
92
|
+
issues: deterministicPrecheck.issues.map((issue) => issue.message),
|
|
93
|
+
timing: buildTiming(runStartedAt),
|
|
94
|
+
});
|
|
82
95
|
runValidator(taskArg);
|
|
83
96
|
console.log(`Checker preflight blocked ${taskId}: return_to_plan`);
|
|
84
97
|
console.log(`- deterministicIssues: ${deterministicPrecheck.issues.length}`);
|
|
@@ -146,6 +159,13 @@ async function runMain() {
|
|
|
146
159
|
cacheKey,
|
|
147
160
|
contextMode,
|
|
148
161
|
});
|
|
162
|
+
appendCheckTimeline(taskDir, {
|
|
163
|
+
event: 'llm_input_built',
|
|
164
|
+
contextMode,
|
|
165
|
+
cacheKeySha,
|
|
166
|
+
packMeta: promptPayload.pack.meta,
|
|
167
|
+
timing: buildTiming(runStartedAt),
|
|
168
|
+
});
|
|
149
169
|
|
|
150
170
|
console.log(`Checker LLM input for ${taskId}`);
|
|
151
171
|
for (const line of summarizePackForConsole(promptPayload.pack)) {
|
|
@@ -169,6 +189,13 @@ async function runMain() {
|
|
|
169
189
|
message: `Strict LLM input pack exceeds cap: estimatedTokens=${promptPayload.pack.meta.estimatedTokens}, capTokens=${promptPayload.pack.meta.capTokens}`,
|
|
170
190
|
rawOutput: null,
|
|
171
191
|
});
|
|
192
|
+
appendCheckTimeline(taskDir, {
|
|
193
|
+
event: 'context_overflow',
|
|
194
|
+
contextMode,
|
|
195
|
+
cacheKeySha,
|
|
196
|
+
packMeta: promptPayload.pack.meta,
|
|
197
|
+
timing: buildTiming(runStartedAt),
|
|
198
|
+
});
|
|
172
199
|
recordLlmInputUsage({
|
|
173
200
|
taskDir,
|
|
174
201
|
stage: 'check',
|
|
@@ -184,6 +211,13 @@ async function runMain() {
|
|
|
184
211
|
|
|
185
212
|
if (!noCache && restoreFromCache({ taskDir, taskArg, cacheKeySha })) {
|
|
186
213
|
llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, 'cache_hit'));
|
|
214
|
+
appendCheckTimeline(taskDir, {
|
|
215
|
+
event: 'cache_hit',
|
|
216
|
+
contextMode,
|
|
217
|
+
cacheKeySha,
|
|
218
|
+
packMeta: promptPayload.pack.meta,
|
|
219
|
+
timing: buildTiming(runStartedAt),
|
|
220
|
+
});
|
|
187
221
|
recordLlmInputUsage({
|
|
188
222
|
taskDir,
|
|
189
223
|
stage: 'check',
|
|
@@ -197,11 +231,32 @@ async function runMain() {
|
|
|
197
231
|
}
|
|
198
232
|
|
|
199
233
|
try {
|
|
234
|
+
const providerStartedAt = new Date();
|
|
235
|
+
appendCheckTimeline(taskDir, {
|
|
236
|
+
event: 'provider_started',
|
|
237
|
+
provider: checkerConfig.provider,
|
|
238
|
+
model: checkerConfig.model,
|
|
239
|
+
reasoningEffort: checkerConfig.reasoningEffort,
|
|
240
|
+
contextMode,
|
|
241
|
+
cacheKeySha,
|
|
242
|
+
packMeta: promptPayload.pack.meta,
|
|
243
|
+
timing: buildTiming(runStartedAt),
|
|
244
|
+
});
|
|
200
245
|
providerOutput = await runProvider({
|
|
201
246
|
checkerConfig,
|
|
202
247
|
messages: promptPayload.messages,
|
|
203
248
|
prompt: promptPayload.prompt,
|
|
204
249
|
});
|
|
250
|
+
appendCheckTimeline(taskDir, {
|
|
251
|
+
event: 'provider_completed',
|
|
252
|
+
provider: checkerConfig.provider,
|
|
253
|
+
model: checkerConfig.model,
|
|
254
|
+
contextMode,
|
|
255
|
+
cacheKeySha,
|
|
256
|
+
verdict: providerOutput.checkResultJson?.verdict || null,
|
|
257
|
+
providerTiming: buildTiming(providerStartedAt),
|
|
258
|
+
timing: buildTiming(runStartedAt),
|
|
259
|
+
});
|
|
205
260
|
} catch (error) {
|
|
206
261
|
const failureReason = error.failureReason || 'unknown';
|
|
207
262
|
writeFailureArtifacts({
|
|
@@ -215,6 +270,16 @@ async function runMain() {
|
|
|
215
270
|
message: error.message,
|
|
216
271
|
rawOutput: error.rawOutput || null,
|
|
217
272
|
});
|
|
273
|
+
appendCheckTimeline(taskDir, {
|
|
274
|
+
event: 'provider_failed',
|
|
275
|
+
provider: checkerConfig.provider,
|
|
276
|
+
model: checkerConfig.model,
|
|
277
|
+
contextMode,
|
|
278
|
+
cacheKeySha,
|
|
279
|
+
failureReason,
|
|
280
|
+
message: error.message,
|
|
281
|
+
timing: buildTiming(runStartedAt),
|
|
282
|
+
});
|
|
218
283
|
llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, `provider_failed:${failureReason}`));
|
|
219
284
|
recordLlmInputUsage({
|
|
220
285
|
taskDir,
|
|
@@ -246,6 +311,13 @@ async function runMain() {
|
|
|
246
311
|
cacheKey,
|
|
247
312
|
providerOutput,
|
|
248
313
|
});
|
|
314
|
+
appendCheckTimeline(taskDir, {
|
|
315
|
+
event: 'check_completed',
|
|
316
|
+
verdict: providerOutput.checkResultJson?.verdict || null,
|
|
317
|
+
contextMode: promptPayload.pack.meta.mode,
|
|
318
|
+
cacheKeySha,
|
|
319
|
+
timing: buildTiming(runStartedAt),
|
|
320
|
+
});
|
|
249
321
|
if (!isContextInsufficientResult(providerOutput.checkResultJson)) {
|
|
250
322
|
storeInCache({ taskDir, cacheKeySha });
|
|
251
323
|
}
|
|
@@ -263,6 +335,26 @@ async function runMain() {
|
|
|
263
335
|
console.log(`- finalEstimatedInputTokens: ${promptPayload.pack.meta.estimatedTokens}`);
|
|
264
336
|
}
|
|
265
337
|
|
|
338
|
+
function appendCheckTimeline(taskDir, event) {
|
|
339
|
+
const timelinePath = path.join(taskDir, 'check-timeline.json');
|
|
340
|
+
let existing = [];
|
|
341
|
+
if (fs.existsSync(timelinePath)) {
|
|
342
|
+
try {
|
|
343
|
+
const parsed = JSON.parse(fs.readFileSync(timelinePath, 'utf8'));
|
|
344
|
+
if (Array.isArray(parsed)) {
|
|
345
|
+
existing = parsed;
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
existing = [];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
existing.push({
|
|
352
|
+
at: new Date().toISOString(),
|
|
353
|
+
...event,
|
|
354
|
+
});
|
|
355
|
+
writeTaskFile(taskDir, 'check-timeline.json', JSON.stringify(existing, null, 2));
|
|
356
|
+
}
|
|
357
|
+
|
|
266
358
|
function buildAttemptRecord(packMeta, outcome) {
|
|
267
359
|
return {
|
|
268
360
|
mode: packMeta.mode,
|