@besales/ops-framework 0.1.13 → 0.1.14
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 +5 -0
- package/README.md +1 -1
- package/bin/initiative.mjs +166 -2
- package/bin/initiative.test.mjs +70 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.14
|
|
4
|
+
|
|
5
|
+
- Added initiative synthesis readiness gates for missing critical concepts, ADR/NFR coverage, scope/storage conflicts, apply-contract ambiguity, sequencing hints and stronger source-reference warnings.
|
|
6
|
+
- Made `initiative-requirements-synthesis` block planning when source docs contain important concepts that synthesized requirements do not explicitly cover.
|
|
7
|
+
|
|
3
8
|
## 0.1.13
|
|
4
9
|
|
|
5
10
|
- Fixed `ops-agent --version` to read the installed package version instead of printing a stale hardcoded value.
|
package/README.md
CHANGED
|
@@ -263,7 +263,7 @@ Raw docs
|
|
|
263
263
|
|
|
264
264
|
`initiative-requirements <initiative>` turns those candidates into `REQ-*` rows in `requirements-map.md`, writes `requirements-review.md` approval cards and writes `coverage.md`. Human review is still required before treating candidates as approved or planned.
|
|
265
265
|
|
|
266
|
-
`initiative-requirements-synthesis <initiative>` writes `requirements-synthesis.md`: the human/LLM synthesis pack for converting raw candidates into clean requirements with acceptance criteria. It highlights source coverage gaps, fragments, table rows, run-on blocks and candidate assumptions/questions that must be rewritten before planning.
|
|
266
|
+
`initiative-requirements-synthesis <initiative>` writes `requirements-synthesis.md`: the human/LLM synthesis pack for converting raw candidates into clean requirements with acceptance criteria. It highlights source coverage gaps, fragments, table rows, run-on blocks and candidate assumptions/questions that must be rewritten before planning. It also runs synthesis readiness gates for missing critical concepts, ADR/NFR coverage, scope/storage conflicts, apply-contract ambiguity, sequencing hints and weak source references.
|
|
267
267
|
|
|
268
268
|
## Feedback Intake
|
|
269
269
|
|
package/bin/initiative.mjs
CHANGED
|
@@ -360,19 +360,24 @@ export function initiativeRequirementsSynthesis({
|
|
|
360
360
|
const questionsPath = path.join(initiative.initiativeDir, 'intake', 'open-questions.md');
|
|
361
361
|
const assumptions = fs.existsSync(assumptionsPath) ? fs.readFileSync(assumptionsPath, 'utf8') : '';
|
|
362
362
|
const questions = fs.existsSync(questionsPath) ? fs.readFileSync(questionsPath, 'utf8') : '';
|
|
363
|
+
const sourceDocs = readIndexedSourceDocs({ projectRoot, sourceIndex });
|
|
364
|
+
const gates = buildSynthesisReadinessGates({ requirements, sourceDocs });
|
|
363
365
|
const rewriteRequired = requirements.some((req) => requirementNeedsRewrite(req))
|
|
364
|
-
|| Boolean(sourceIndex.sourceCoverage?.relevantExcludedSources?.length)
|
|
366
|
+
|| Boolean(sourceIndex.sourceCoverage?.relevantExcludedSources?.length)
|
|
367
|
+
|| gates.some((gate) => gate.blocking);
|
|
365
368
|
const changes = [];
|
|
366
369
|
writeFileIfAllowed(path.join(initiative.initiativeDir, 'requirements-synthesis.md'), renderRequirementsSynthesis({
|
|
367
370
|
sourceCoverage: sourceIndex.sourceCoverage,
|
|
368
371
|
requirements,
|
|
369
372
|
assumptions,
|
|
370
373
|
questions,
|
|
374
|
+
gates,
|
|
371
375
|
rewriteRequired,
|
|
372
376
|
}), { force: true, changes });
|
|
373
377
|
return {
|
|
374
378
|
initiativeId,
|
|
375
379
|
requirements,
|
|
380
|
+
gates,
|
|
376
381
|
rewriteRequired,
|
|
377
382
|
changes,
|
|
378
383
|
};
|
|
@@ -987,8 +992,151 @@ function parseRequirementsMap(content) {
|
|
|
987
992
|
}).filter((req) => req.id && req.source && req.text);
|
|
988
993
|
}
|
|
989
994
|
|
|
990
|
-
function
|
|
995
|
+
function readIndexedSourceDocs({ projectRoot, sourceIndex }) {
|
|
996
|
+
return (sourceIndex.sources || []).map((source) => {
|
|
997
|
+
const filePath = path.join(projectRoot, source.path);
|
|
998
|
+
return {
|
|
999
|
+
...source,
|
|
1000
|
+
content: fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '',
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function buildSynthesisReadinessGates({ requirements, sourceDocs }) {
|
|
1006
|
+
const sourceText = sourceDocs.map((source) => source.content).join('\n');
|
|
1007
|
+
const requirementText = requirements.map((req) => [
|
|
1008
|
+
req.text,
|
|
1009
|
+
req.acceptance,
|
|
1010
|
+
req.notes,
|
|
1011
|
+
req.workPackage,
|
|
1012
|
+
].filter(Boolean).join('\n')).join('\n');
|
|
1013
|
+
const gates = [];
|
|
1014
|
+
addMissingConceptGate({
|
|
1015
|
+
gates,
|
|
1016
|
+
sourceText,
|
|
1017
|
+
requirementText,
|
|
1018
|
+
id: 'critical-concept-speaker-resolution',
|
|
1019
|
+
title: 'Speaker identity resolution and person aliases',
|
|
1020
|
+
sourcePattern: /(speaker[_ -]?resolution|single[- ]label|person_aliases|speaker_resolution_status|committed_by_person|alias(?:es)?)/i,
|
|
1021
|
+
requirementPattern: /(speaker[_ -]?resolution|person_aliases|speaker_resolution_status|speaker identity|alias(?:es)?)/i,
|
|
1022
|
+
severity: 'critical',
|
|
1023
|
+
action: 'Add a Phase 1 requirement for speaker identity resolution and include alias storage/schema fields before planning meeting extraction.',
|
|
1024
|
+
});
|
|
1025
|
+
addMissingConceptGate({
|
|
1026
|
+
gates,
|
|
1027
|
+
sourceText,
|
|
1028
|
+
requirementText,
|
|
1029
|
+
id: 'nfr-attribution-audit-access',
|
|
1030
|
+
title: 'Attribution, audit and access contract',
|
|
1031
|
+
sourcePattern: /(created_by|app\.current_user_id|owner_id|entity_history|enforcement[- ]deferred|audit|attribution)/i,
|
|
1032
|
+
requirementPattern: /(created_by|app\.current_user_id|owner_id|entity_history|enforcement[- ]deferred|audit|attribution|access)/i,
|
|
1033
|
+
severity: 'critical',
|
|
1034
|
+
action: 'Add an explicit Phase 1 platform/NFR requirement for write attribution, owner assignment, audit history and deferred enforcement.',
|
|
1035
|
+
});
|
|
1036
|
+
addScopeStorageConflictGate({ gates, sourceText, requirementText });
|
|
1037
|
+
addApplyContractGate({ gates, sourceText, requirementText });
|
|
1038
|
+
addSequencingGate({ gates, sourceText, requirementText });
|
|
1039
|
+
addStrongerSourceRefGate({ gates, sourceDocs, requirements });
|
|
1040
|
+
return gates;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function addMissingConceptGate({
|
|
1044
|
+
gates,
|
|
1045
|
+
sourceText,
|
|
1046
|
+
requirementText,
|
|
1047
|
+
id,
|
|
1048
|
+
title,
|
|
1049
|
+
sourcePattern,
|
|
1050
|
+
requirementPattern,
|
|
1051
|
+
severity,
|
|
1052
|
+
action,
|
|
1053
|
+
}) {
|
|
1054
|
+
if (sourcePattern.test(sourceText) && !requirementPattern.test(requirementText)) {
|
|
1055
|
+
gates.push({
|
|
1056
|
+
id,
|
|
1057
|
+
severity,
|
|
1058
|
+
blocking: true,
|
|
1059
|
+
title,
|
|
1060
|
+
issue: 'Source docs mention this concept, but synthesized requirements do not cover it explicitly.',
|
|
1061
|
+
action,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function addScopeStorageConflictGate({ gates, sourceText, requirementText }) {
|
|
1067
|
+
const mentionsWideTrackTaxonomy = /(tracker|advisor[_ -]?network|investor|grant[_ -]?funding)/i.test(requirementText);
|
|
1068
|
+
const defersFactStorage = /(commitments?|decisions?|risks?|related_person_id).{0,120}(phase\s*2|future|defer|отлож)/is.test(`${requirementText}\n${sourceText}`)
|
|
1069
|
+
|| /(phase\s*2|future|defer|отлож).{0,120}(commitments?|decisions?|risks?|related_person_id)/is.test(`${requirementText}\n${sourceText}`);
|
|
1070
|
+
const hasExplicitTrackScopeDecision = /(client\s*\+\s*internal|client and internal|только client|только клиент|only client|track[- ]scope|6 tracks?.{0,80}(phase\s*2|future|defer))/i.test(requirementText);
|
|
1071
|
+
if (mentionsWideTrackTaxonomy && defersFactStorage && !hasExplicitTrackScopeDecision) {
|
|
1072
|
+
gates.push({
|
|
1073
|
+
id: 'scope-storage-track-conflict',
|
|
1074
|
+
severity: 'needs_decision',
|
|
1075
|
+
blocking: true,
|
|
1076
|
+
title: 'Track taxonomy conflicts with deferred fact storage',
|
|
1077
|
+
issue: 'Requirements mention non-client/internal tracks while storage for commitments/decisions/risks or related-person facts appears deferred.',
|
|
1078
|
+
action: 'Decide explicitly: Phase 1 taxonomy is client+internal only, or Phase 1 includes minimal fact storage for the wider track set.',
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function addApplyContractGate({ gates, sourceText, requirementText }) {
|
|
1084
|
+
const hasApplyFlow = /(apply|approval|proposedchange|proposed change|process-meeting|process meeting|\/process-meeting)/i.test(requirementText);
|
|
1085
|
+
const hasDeferredFacts = /(commitments?|decisions?|risks?|projects?).{0,120}(phase\s*2|future|defer|отлож)/is.test(`${requirementText}\n${sourceText}`)
|
|
1086
|
+
|| /(phase\s*2|future|defer|отлож).{0,120}(commitments?|decisions?|risks?|projects?)/is.test(`${requirementText}\n${sourceText}`);
|
|
1087
|
+
const definesDraftOnlyContract = /(draft|proposed only|not applied|не применяется|чернов|только proposed|only proposed)/i.test(requirementText);
|
|
1088
|
+
if (hasApplyFlow && hasDeferredFacts && !definesDraftOnlyContract) {
|
|
1089
|
+
gates.push({
|
|
1090
|
+
id: 'apply-contract-ambiguous',
|
|
1091
|
+
severity: 'needs_decision',
|
|
1092
|
+
blocking: true,
|
|
1093
|
+
title: 'Phase apply contract is ambiguous',
|
|
1094
|
+
issue: 'Apply/approval flow exists, but some extracted fact domains are deferred. Requirements do not say what is applied versus kept as draft/proposed.',
|
|
1095
|
+
action: 'State acceptance for Phase 1 apply: which entities are written, which outputs remain draft ProposedChange, and which domains are out of scope.',
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function addSequencingGate({ gates, sourceText, requirementText }) {
|
|
1101
|
+
const hasGoldenSet = /(golden set|golden-set|regression baseline|baseline regression)/i.test(`${sourceText}\n${requirementText}`);
|
|
1102
|
+
const hasMeetingExtraction = /(process-meeting|process meeting|\/process-meeting|meeting extraction)/i.test(requirementText);
|
|
1103
|
+
const hasOrdering = /(golden set|golden-set).{0,120}(before|parallel|перед|раньше|параллель)/is.test(requirementText)
|
|
1104
|
+
|| /(before|parallel|перед|раньше|параллель).{0,120}(golden set|golden-set)/is.test(requirementText);
|
|
1105
|
+
if (hasGoldenSet && hasMeetingExtraction && !hasOrdering) {
|
|
1106
|
+
gates.push({
|
|
1107
|
+
id: 'sequencing-golden-set-before-extraction',
|
|
1108
|
+
severity: 'planning_hint',
|
|
1109
|
+
blocking: false,
|
|
1110
|
+
title: 'Golden set should baseline meeting extraction',
|
|
1111
|
+
issue: 'Sources mention golden set/regression baseline and requirements mention meeting extraction, but sequencing is not explicit.',
|
|
1112
|
+
action: 'Plan golden set before or parallel to /process-meeting implementation so quality has a baseline.',
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function addStrongerSourceRefGate({ gates, sourceDocs, requirements }) {
|
|
1118
|
+
const feedbackSources = sourceDocs.filter((source) => /feedback/i.test(source.path));
|
|
1119
|
+
if (!feedbackSources.length) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const feedbackText = feedbackSources.map((source) => source.content).join('\n');
|
|
1123
|
+
for (const req of requirements) {
|
|
1124
|
+
if (/backfill/i.test(req.text) && /backfill/i.test(feedbackText) && !/feedback/i.test(req.source)) {
|
|
1125
|
+
gates.push({
|
|
1126
|
+
id: `stronger-source-ref-${req.id}`,
|
|
1127
|
+
severity: 'traceability',
|
|
1128
|
+
blocking: false,
|
|
1129
|
+
title: `Stronger source reference available for ${req.id}`,
|
|
1130
|
+
issue: 'Requirement mentions backfill, and feedback docs also document backfill decisions, but the requirement source is not a feedback log.',
|
|
1131
|
+
action: 'Add feedback-log source refs or derived-from references before approval.',
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions, questions, gates = [], rewriteRequired }) {
|
|
991
1138
|
const rewriteCandidates = requirements.filter((req) => requirementNeedsRewrite(req));
|
|
1139
|
+
const blockingGates = gates.filter((gate) => gate.blocking);
|
|
992
1140
|
return [
|
|
993
1141
|
'# Requirements Synthesis Pack',
|
|
994
1142
|
'',
|
|
@@ -996,6 +1144,7 @@ function renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions
|
|
|
996
1144
|
`- Requirement candidates: ${requirements.length}`,
|
|
997
1145
|
`- Candidates needing rewrite: ${rewriteCandidates.length}`,
|
|
998
1146
|
`- Relevant sources still missing: ${sourceCoverage?.relevantExcludedSources?.length || 0}`,
|
|
1147
|
+
`- Blocking readiness gates: ${blockingGates.length}`,
|
|
999
1148
|
'',
|
|
1000
1149
|
'## Source Coverage Gate',
|
|
1001
1150
|
'',
|
|
@@ -1009,6 +1158,21 @@ function renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions
|
|
|
1009
1158
|
? rewriteCandidates.map((req) => `- ${req.id} (${req.quality}): ${req.qualityIssue || 'Rewrite into a clean requirement with acceptance.'}`)
|
|
1010
1159
|
: ['- None.']),
|
|
1011
1160
|
'',
|
|
1161
|
+
'## Synthesis Readiness Gates',
|
|
1162
|
+
'',
|
|
1163
|
+
...(gates.length
|
|
1164
|
+
? gates.map((gate) => [
|
|
1165
|
+
`### ${gate.id}`,
|
|
1166
|
+
'',
|
|
1167
|
+
`- Severity: \`${gate.severity}\``,
|
|
1168
|
+
`- Blocking: \`${gate.blocking ? 'yes' : 'no'}\``,
|
|
1169
|
+
`- Title: ${gate.title}`,
|
|
1170
|
+
`- Issue: ${gate.issue}`,
|
|
1171
|
+
`- Required action: ${gate.action}`,
|
|
1172
|
+
'',
|
|
1173
|
+
].join('\n'))
|
|
1174
|
+
: ['- None.']),
|
|
1175
|
+
'',
|
|
1012
1176
|
'## Synthesis Instructions',
|
|
1013
1177
|
'',
|
|
1014
1178
|
'Create final requirements only after human/LLM synthesis. Each final requirement must have:',
|
package/bin/initiative.test.mjs
CHANGED
|
@@ -375,6 +375,76 @@ describe('initiative framework', () => {
|
|
|
375
375
|
expect(pack).toContain('Requirements Synthesis Pack');
|
|
376
376
|
expect(pack).toContain('Synthesized requirement');
|
|
377
377
|
});
|
|
378
|
+
|
|
379
|
+
it('adds synthesis readiness gates for missing concepts, scope conflicts and sequencing', () => {
|
|
380
|
+
const root = makeProject();
|
|
381
|
+
const docsDir = path.join(root, 'docs', 'delivery-os');
|
|
382
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
383
|
+
fs.writeFileSync(path.join(docsDir, '06-roadmap.md'), [
|
|
384
|
+
'# Phase 1',
|
|
385
|
+
'',
|
|
386
|
+
'- MVP taxonomy must cover client, internal, tracker, advisor_network, investor, grant_funding.',
|
|
387
|
+
'- System must implement /process-meeting approval and apply.',
|
|
388
|
+
'- Golden set baseline Week 1-2 for meeting extraction regression.',
|
|
389
|
+
'- Migration 001b: person_aliases. Meeting fields: meeting_track, series_id, asr_quality, speaker_resolution_status.',
|
|
390
|
+
'- Analysis found single-label exports, alias zoo, committed_by_person attribution risk.',
|
|
391
|
+
'- Phase 2 defers commitments decisions risks related_person_id storage.',
|
|
392
|
+
].join('\n'));
|
|
393
|
+
fs.writeFileSync(path.join(docsDir, '07-adrs.md'), [
|
|
394
|
+
'# ADR-6',
|
|
395
|
+
'',
|
|
396
|
+
'- Schema-ready enforcement-deferred: created_by, app.current_user_id, owner_id, entity_history.',
|
|
397
|
+
].join('\n'));
|
|
398
|
+
fs.writeFileSync(path.join(docsDir, '05-skills-catalog.md'), [
|
|
399
|
+
'# Skills',
|
|
400
|
+
'',
|
|
401
|
+
'- Backfill must stay split from Phase 1 rollout.',
|
|
402
|
+
].join('\n'));
|
|
403
|
+
fs.writeFileSync(path.join(docsDir, '99-feedback-log.md'), [
|
|
404
|
+
'# Feedback',
|
|
405
|
+
'',
|
|
406
|
+
'- Decision: backfill split is documented here with the stronger source reference.',
|
|
407
|
+
].join('\n'));
|
|
408
|
+
createInitiative({
|
|
409
|
+
projectRoot: root,
|
|
410
|
+
initiativeId: 'delivery-os-mvp',
|
|
411
|
+
title: 'Delivery OS MVP',
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
initiativeIntake({
|
|
415
|
+
projectRoot: root,
|
|
416
|
+
initiativeId: 'delivery-os-mvp',
|
|
417
|
+
docsDir: 'docs/delivery-os',
|
|
418
|
+
phase: 'phase-1',
|
|
419
|
+
});
|
|
420
|
+
initiativeRequirements({
|
|
421
|
+
projectRoot: root,
|
|
422
|
+
initiativeId: 'delivery-os-mvp',
|
|
423
|
+
phase: 'phase-1',
|
|
424
|
+
});
|
|
425
|
+
const synthesis = initiativeRequirementsSynthesis({
|
|
426
|
+
projectRoot: root,
|
|
427
|
+
initiativeId: 'delivery-os-mvp',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const gateIds = synthesis.gates.map((gate) => gate.id);
|
|
431
|
+
expect(synthesis.rewriteRequired).toBe(true);
|
|
432
|
+
expect(gateIds).toContain('critical-concept-speaker-resolution');
|
|
433
|
+
expect(gateIds).toContain('nfr-attribution-audit-access');
|
|
434
|
+
expect(gateIds).toContain('scope-storage-track-conflict');
|
|
435
|
+
expect(gateIds).toContain('apply-contract-ambiguous');
|
|
436
|
+
expect(gateIds).toContain('sequencing-golden-set-before-extraction');
|
|
437
|
+
expect(gateIds.some((id) => id.startsWith('stronger-source-ref-'))).toBe(true);
|
|
438
|
+
|
|
439
|
+
const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
|
|
440
|
+
const pack = fs.readFileSync(path.join(initiativeDir, 'requirements-synthesis.md'), 'utf8');
|
|
441
|
+
expect(pack).toContain('Synthesis Readiness Gates');
|
|
442
|
+
expect(pack).toContain('Speaker identity resolution and person aliases');
|
|
443
|
+
expect(pack).toContain('Track taxonomy conflicts with deferred fact storage');
|
|
444
|
+
expect(pack).toContain('Phase apply contract is ambiguous');
|
|
445
|
+
expect(pack).toContain('Golden set should baseline meeting extraction');
|
|
446
|
+
expect(pack).toContain('Stronger source reference available');
|
|
447
|
+
});
|
|
378
448
|
});
|
|
379
449
|
|
|
380
450
|
function makeProject() {
|