@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 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
 
@@ -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 renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions, questions, rewriteRequired }) {
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:',
@@ -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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/ops-framework",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ops-agent": "bin/ops-agent.mjs"