@besales/ops-framework 0.1.18 → 0.1.21
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/bin/initiative.mjs +61 -0
- package/bin/initiative.test.mjs +114 -0
- package/bin/lib/check-context-utils.mjs +251 -2
- package/bin/lib/check-context-utils.test.mjs +260 -0
- package/bin/lib/execution-ledger-utils.mjs +22 -2
- package/bin/lib/execution-ledger-utils.test.mjs +60 -0
- package/bin/lib/llm-input-pack-utils.mjs +89 -19
- package/bin/lib/llm-input-pack-utils.test.mjs +11 -4
- package/bin/lib/task-manifest-utils.test.mjs +22 -0
- package/bin/run-check.mjs +92 -0
- package/bin/run-verify.mjs +106 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.21
|
|
4
|
+
|
|
5
|
+
- Added `verify-timeline.json` telemetry for Verify runs, including deterministic blocks, LLM input sizing, context-mode escalation, provider duration/failure and final verdict.
|
|
6
|
+
- Prevented Verify artifact-growth loops by compacting generated review/log artifacts (`verify.md`, `check.md`, `check-resolution.md`, `orchestration-log.md`) even in strict verifier context while preserving full execution evidence.
|
|
7
|
+
- Compacted `execution-ledger.json` in verifier input and marked files mentioned in `execution.md` so task-scope files do not remain in `unrelatedDirtyFiles`.
|
|
8
|
+
|
|
9
|
+
## 0.1.20
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## 0.1.19
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
- Tightened O3 optimization gating: plans must include a representative measurement path or explicit blocked/human-owned fallback before external Check.
|
|
19
|
+
- Added `check-timeline.json` telemetry for Check runs, including deterministic blocks, LLM input sizing, cache hits, provider start/completion/failure and context overflow events.
|
|
20
|
+
|
|
3
21
|
## 0.1.18
|
|
4
22
|
|
|
5
23
|
- Added work-package dependency metadata (`Depends on`, `Unblocks`, `Can run parallel with`, `Sequencing notes`) to new initiative work packages.
|
package/bin/initiative.mjs
CHANGED
|
@@ -1038,7 +1038,9 @@ function buildSynthesisReadinessGates({ requirements, sourceDocs }) {
|
|
|
1038
1038
|
});
|
|
1039
1039
|
addScopeStorageConflictGate({ gates, sourceText, requirementText });
|
|
1040
1040
|
addApplyContractGate({ gates, sourceText, requirementText });
|
|
1041
|
+
addDraftStorageShapeGate({ gates, sourceText, requirementText });
|
|
1041
1042
|
addSchemaCompatibilityGate({ gates, sourceText, requirements });
|
|
1043
|
+
addAuditWriterModelGate({ gates, sourceText, requirementText });
|
|
1042
1044
|
addSchemaAttributionWarningGate({ gates, sourceText, requirements });
|
|
1043
1045
|
addSequencingGate({ gates, sourceText, requirementText });
|
|
1044
1046
|
addStrongerSourceRefGate({ gates, sourceDocs, requirements });
|
|
@@ -1105,6 +1107,65 @@ function addApplyContractGate({ gates, sourceText, requirementText }) {
|
|
|
1105
1107
|
}
|
|
1106
1108
|
}
|
|
1107
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
|
+
|
|
1108
1169
|
function addSchemaCompatibilityGate({ gates, sourceText, requirements }) {
|
|
1109
1170
|
const schemaReq = findSchemaRequirement(requirements);
|
|
1110
1171
|
if (!schemaReq) {
|
package/bin/initiative.test.mjs
CHANGED
|
@@ -783,6 +783,120 @@ describe('initiative framework', () => {
|
|
|
783
783
|
expect(schemaGate.issue).toContain('meeting_track');
|
|
784
784
|
expect(schemaGate.action).toContain('meetings.raw_speaker_labels');
|
|
785
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
|
+
});
|
|
786
900
|
});
|
|
787
901
|
|
|
788
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
|
}
|