@besales/ops-framework 0.1.14 → 0.1.16

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.16
4
+
5
+ - Added a schema compatibility readiness gate that blocks planning when a closed/minimal schema requirement does not support dependent approved requirements such as speaker resolution, aliases and meeting track classification.
6
+ - Added a schema attribution/access warning when sources mention `created_by`, `app.current_user_id`, `owner_id`, `entity_history` or deferred enforcement but the schema requirement omits them.
7
+
8
+ ## 0.1.15
9
+
10
+ - Strengthened the initiative track-scope/storage synthesis gate to detect wide meeting tracks routed to commitments/decisions/risks via `related_person_id` when that storage is deferred.
11
+ - Linked the track-scope gate action to the apply-contract decision so synthesis resolves both scope and write semantics together.
12
+
3
13
  ## 0.1.14
4
14
 
5
15
  - 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.
@@ -1035,6 +1035,8 @@ function buildSynthesisReadinessGates({ requirements, sourceDocs }) {
1035
1035
  });
1036
1036
  addScopeStorageConflictGate({ gates, sourceText, requirementText });
1037
1037
  addApplyContractGate({ gates, sourceText, requirementText });
1038
+ addSchemaCompatibilityGate({ gates, sourceText, requirements });
1039
+ addSchemaAttributionWarningGate({ gates, sourceText, requirements });
1038
1040
  addSequencingGate({ gates, sourceText, requirementText });
1039
1041
  addStrongerSourceRefGate({ gates, sourceDocs, requirements });
1040
1042
  return gates;
@@ -1064,18 +1066,21 @@ function addMissingConceptGate({
1064
1066
  }
1065
1067
 
1066
1068
  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}`);
1069
+ const combinedText = `${requirementText}\n${sourceText}`;
1070
+ const mentionsWideTrackTaxonomy = /(tracker|advisor[_ -]?network|advisor-network|investor|grant[_ -]?funding|fundraising|grants?)/i.test(combinedText);
1071
+ const mapsWideTracksToFactStorage = /(advisor[_ -]?network|advisor-network|fundraising|investor|grant[_ -]?funding|grants?|tracker).{0,240}(commitments?|decisions?|risks?|related_person_id|nullable client_id|meeting_track\s+in)/is.test(sourceText)
1072
+ || /(commitments?|decisions?|risks?|related_person_id|nullable client_id|meeting_track\s+in).{0,240}(advisor[_ -]?network|advisor-network|fundraising|investor|grant[_ -]?funding|grants?|tracker)/is.test(sourceText);
1073
+ const defersFactStorage = /(commitments?|decisions?|risks?|related_person_id).{0,160}(phase\s*2|future|defer|deferred|отлож)/is.test(combinedText)
1074
+ || /(phase\s*2|future|defer|deferred|отлож).{0,160}(commitments?|decisions?|risks?|related_person_id)/is.test(combinedText);
1070
1075
  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) {
1076
+ if (mentionsWideTrackTaxonomy && mapsWideTracksToFactStorage && defersFactStorage && !hasExplicitTrackScopeDecision) {
1072
1077
  gates.push({
1073
1078
  id: 'scope-storage-track-conflict',
1074
1079
  severity: 'needs_decision',
1075
1080
  blocking: true,
1076
1081
  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.',
1082
+ issue: 'Sources connect non-client/internal meeting tracks to commitments/decisions/risks or related-person facts while that storage appears deferred.',
1083
+ action: 'Decide explicitly with the apply-contract gate: Phase 1 extraction is client+internal only, or Phase 1 includes minimal commitments/decisions/risks storage with related_person_id for the wider track set.',
1079
1084
  });
1080
1085
  }
1081
1086
  }
@@ -1097,6 +1102,82 @@ function addApplyContractGate({ gates, sourceText, requirementText }) {
1097
1102
  }
1098
1103
  }
1099
1104
 
1105
+ function addSchemaCompatibilityGate({ gates, sourceText, requirements }) {
1106
+ const schemaReq = findSchemaRequirement(requirements);
1107
+ if (!schemaReq) {
1108
+ return;
1109
+ }
1110
+ const schemaText = requirementFullText(schemaReq);
1111
+ const allRequirementText = requirements.map((req) => requirementFullText(req)).join('\n');
1112
+ const schemaLooksClosed = /(includes?\s+only|only\s+includes?|schema\s+includes\s+only|только|исключительно)/i.test(schemaText);
1113
+ const hasSpeakerResolutionRequirement = /(speaker[_ -]?resolution|speaker identity|raw speaker|raw_speaker|person_aliases|aliases?|alias-хранилище)/i.test(allRequirementText);
1114
+ const schemaSupportsSpeakerResolution = /(person_aliases|raw_speaker_labels?|speaker_resolution_status|speaker alias|speaker label|aliases?)/i.test(schemaText);
1115
+ const hasTrackClassificationRequirement = /(meeting_track|track classification|classif(?:y|ication).{0,80}track|классификац.{0,80}трек)/i.test(`${allRequirementText}\n${sourceText}`);
1116
+ const schemaSupportsTrackClassification = /meeting_track/i.test(schemaText);
1117
+ const sourceMentionsMeetingOperationalFields = /(series_id|asr_quality)/i.test(sourceText);
1118
+ const schemaSupportsMeetingOperationalFields = !sourceMentionsMeetingOperationalFields || /(series_id|asr_quality)/i.test(schemaText);
1119
+ const missing = [];
1120
+ if (hasSpeakerResolutionRequirement && !schemaSupportsSpeakerResolution) {
1121
+ missing.push('speaker resolution storage (`person_aliases` or `raw_speaker_labels` + `speaker_resolution_status`)');
1122
+ }
1123
+ if (hasTrackClassificationRequirement && !schemaSupportsTrackClassification) {
1124
+ missing.push('`meeting_track` on meetings');
1125
+ }
1126
+ if (!schemaSupportsMeetingOperationalFields) {
1127
+ missing.push('meeting operational fields mentioned by sources, such as `series_id` and `asr_quality`');
1128
+ }
1129
+ if (missing.length && schemaLooksClosed) {
1130
+ gates.push({
1131
+ id: 'schema-supports-derived-requirements',
1132
+ severity: 'critical',
1133
+ blocking: true,
1134
+ title: 'Schema requirement does not support dependent requirements',
1135
+ issue: `Schema requirement ${schemaReq.id} is written as a closed/minimal schema list but omits ${missing.join(', ')}.`,
1136
+ action: 'Update the schema requirement before WP planning: add the missing schema support, or explicitly state the alternative Phase 1 storage contract and defer the dependent behavior.',
1137
+ });
1138
+ }
1139
+ }
1140
+
1141
+ function addSchemaAttributionWarningGate({ gates, sourceText, requirements }) {
1142
+ const schemaReq = findSchemaRequirement(requirements);
1143
+ if (!schemaReq) {
1144
+ return;
1145
+ }
1146
+ const schemaText = requirementFullText(schemaReq);
1147
+ const sourceMentionsAttribution = /(created_by|app\.current_user_id|owner_id|entity_history|enforcement[- ]deferred)/i.test(sourceText);
1148
+ const missing = [
1149
+ ['created_by', /created_by/i],
1150
+ ['app.current_user_id', /app\.current_user_id/i],
1151
+ ['owner_id', /owner_id/i],
1152
+ ['entity_history', /entity_history/i],
1153
+ ['enforcement-deferred', /enforcement[- ]deferred/i],
1154
+ ].filter(([, pattern]) => !pattern.test(schemaText)).map(([name]) => name);
1155
+ if (sourceMentionsAttribution && missing.length) {
1156
+ gates.push({
1157
+ id: 'schema-attribution-nfr-warning',
1158
+ severity: 'planning_hint',
1159
+ blocking: false,
1160
+ title: 'Schema attribution/access fields are not explicit',
1161
+ issue: `Sources mention attribution/access schema decisions, but schema requirement ${schemaReq.id} omits ${missing.join(', ')}.`,
1162
+ action: 'Before WP-001, add acceptance for created_by/app.current_user_id, owner_id, entity_history and deferred role enforcement if these are Phase 1 schema foundations.',
1163
+ });
1164
+ }
1165
+ }
1166
+
1167
+ function findSchemaRequirement(requirements) {
1168
+ return requirements.find((req) => /(schema|migration|tables?|таблиц|схем)/i.test(requirementFullText(req)));
1169
+ }
1170
+
1171
+ function requirementFullText(req) {
1172
+ return [
1173
+ req.id,
1174
+ req.text,
1175
+ req.acceptance,
1176
+ req.notes,
1177
+ req.workPackage,
1178
+ ].filter(Boolean).join('\n');
1179
+ }
1180
+
1100
1181
  function addSequencingGate({ gates, sourceText, requirementText }) {
1101
1182
  const hasGoldenSet = /(golden set|golden-set|regression baseline|baseline regression)/i.test(`${sourceText}\n${requirementText}`);
1102
1183
  const hasMeetingExtraction = /(process-meeting|process meeting|\/process-meeting|meeting extraction)/i.test(requirementText);
@@ -388,6 +388,7 @@ describe('initiative framework', () => {
388
388
  '- Golden set baseline Week 1-2 for meeting extraction regression.',
389
389
  '- Migration 001b: person_aliases. Meeting fields: meeting_track, series_id, asr_quality, speaker_resolution_status.',
390
390
  '- Analysis found single-label exports, alias zoo, committed_by_person attribution risk.',
391
+ '- advisor_network, fundraising and grants are routed into commitments, decisions and risks by meeting_track.',
391
392
  '- Phase 2 defers commitments decisions risks related_person_id storage.',
392
393
  ].join('\n'));
393
394
  fs.writeFileSync(path.join(docsDir, '07-adrs.md'), [
@@ -445,6 +446,153 @@ describe('initiative framework', () => {
445
446
  expect(pack).toContain('Golden set should baseline meeting extraction');
446
447
  expect(pack).toContain('Stronger source reference available');
447
448
  });
449
+
450
+ it('detects track scope storage conflicts when routing evidence is only in source docs', () => {
451
+ const root = makeProject();
452
+ const docsDir = path.join(root, 'docs', 'delivery-os');
453
+ fs.mkdirSync(docsDir, { recursive: true });
454
+ fs.writeFileSync(path.join(docsDir, '06-roadmap.md'), [
455
+ '# Phase 1',
456
+ '',
457
+ '- MVP taxonomy must cover client, internal, tracker, advisor_network, investor, grant_funding.',
458
+ '- Minimal Phase 1 schema defers commitments, decisions, risks and related_person_id storage to Phase 2.',
459
+ ].join('\n'));
460
+ fs.writeFileSync(path.join(docsDir, '12-para-workspace-contract.md'), [
461
+ '# Workspace Contract',
462
+ '',
463
+ '- advisor-network / fundraising / grants are routed into commitments, decisions and risks where meeting_track IN (...).',
464
+ ].join('\n'));
465
+ fs.writeFileSync(path.join(docsDir, '13-meeting-extraction-profiles.md'), [
466
+ '# Profiles',
467
+ '',
468
+ '- Non-client tracks require nullable client_id plus related_person_id on commitments, decisions and risks.',
469
+ ].join('\n'));
470
+ createInitiative({
471
+ projectRoot: root,
472
+ initiativeId: 'delivery-os-mvp',
473
+ title: 'Delivery OS MVP',
474
+ });
475
+
476
+ initiativeIntake({
477
+ projectRoot: root,
478
+ initiativeId: 'delivery-os-mvp',
479
+ docsDir: 'docs/delivery-os',
480
+ phase: 'phase-1',
481
+ });
482
+ initiativeRequirements({
483
+ projectRoot: root,
484
+ initiativeId: 'delivery-os-mvp',
485
+ phase: 'phase-1',
486
+ });
487
+ const synthesis = initiativeRequirementsSynthesis({
488
+ projectRoot: root,
489
+ initiativeId: 'delivery-os-mvp',
490
+ });
491
+
492
+ const gate = synthesis.gates.find((item) => item.id === 'scope-storage-track-conflict');
493
+ expect(gate).toMatchObject({
494
+ severity: 'needs_decision',
495
+ blocking: true,
496
+ });
497
+ expect(gate.action).toContain('client+internal');
498
+ expect(gate.action).toContain('related_person_id');
499
+ });
500
+
501
+ it('blocks planning when closed schema requirements do not support dependent approved requirements', () => {
502
+ const root = makeProject();
503
+ const docsDir = path.join(root, 'docs', 'delivery-os');
504
+ fs.mkdirSync(docsDir, { recursive: true });
505
+ fs.writeFileSync(path.join(docsDir, '06-roadmap.md'), [
506
+ '# Phase 1',
507
+ '',
508
+ '- Migration 001b: person_aliases. Meeting fields: meeting_track, series_id, asr_quality, speaker_resolution_status.',
509
+ ].join('\n'));
510
+ fs.writeFileSync(path.join(docsDir, '07-adrs.md'), [
511
+ '# ADR-6',
512
+ '',
513
+ '- Schema-ready enforcement-deferred: created_by, app.current_user_id, owner_id, entity_history.',
514
+ ].join('\n'));
515
+ createInitiative({
516
+ projectRoot: root,
517
+ initiativeId: 'delivery-os-mvp',
518
+ title: 'Delivery OS MVP',
519
+ });
520
+ initiativeIntake({
521
+ projectRoot: root,
522
+ initiativeId: 'delivery-os-mvp',
523
+ docsDir: 'docs/delivery-os',
524
+ phase: 'phase-1',
525
+ });
526
+ const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
527
+ fs.writeFileSync(path.join(initiativeDir, 'requirements-map.md'), [
528
+ '# Requirements Map',
529
+ '',
530
+ '## Requirement Details',
531
+ '',
532
+ '### REQ-002',
533
+ '',
534
+ '- Status: `approved`',
535
+ '- Source: `docs/delivery-os/04-data-model.md:1206`',
536
+ '- Source hash: `sha`',
537
+ '- Phase: `phase-1`',
538
+ '- Quality: `complete`',
539
+ '- Quality issue: (none)',
540
+ '- Work package: WP-001',
541
+ '- Acceptance: Schema includes only team_members, clients, people, meetings, evidence, change_groups, proposed_changes, skill_executions, entity_history.',
542
+ '- Decision: `approve`',
543
+ '- Notes: Human-approved minimal schema.',
544
+ '- Requirement: Minimal Phase 1 schema includes only team_members, clients, people, meetings, evidence, change_groups, proposed_changes, skill_executions, entity_history.',
545
+ '',
546
+ '### REQ-003',
547
+ '',
548
+ '- Status: `approved`',
549
+ '- Source: `docs/delivery-os/06-roadmap.md:10`',
550
+ '- Source hash: `sha`',
551
+ '- Phase: `phase-1`',
552
+ '- Quality: `complete`',
553
+ '- Quality issue: (none)',
554
+ '- Work package: WP-003',
555
+ '- Acceptance: Meetings are classified with `meeting_track`.',
556
+ '- Decision: `approve`',
557
+ '- Notes: (empty)',
558
+ '- Requirement: Phase 1 classifies meetings by track using meeting_track.',
559
+ '',
560
+ '### REQ-004',
561
+ '',
562
+ '- Status: `approved`',
563
+ '- Source: `docs/delivery-os/06-roadmap.md:11`',
564
+ '- Source hash: `sha`',
565
+ '- Phase: `phase-1`',
566
+ '- Quality: `complete`',
567
+ '- Quality issue: (none)',
568
+ '- Work package: WP-001, WP-003',
569
+ '- Acceptance: Speaker resolution stores raw speaker labels and aliases and does not attribute unresolved aliases automatically.',
570
+ '- Decision: `approve`',
571
+ '- Notes: (empty)',
572
+ '- Requirement: Phase 1 supports speaker identity resolution by storing raw speaker labels and aliases.',
573
+ '',
574
+ ].join('\n'));
575
+
576
+ const synthesis = initiativeRequirementsSynthesis({
577
+ projectRoot: root,
578
+ initiativeId: 'delivery-os-mvp',
579
+ });
580
+
581
+ const schemaGate = synthesis.gates.find((gate) => gate.id === 'schema-supports-derived-requirements');
582
+ const attributionGate = synthesis.gates.find((gate) => gate.id === 'schema-attribution-nfr-warning');
583
+ expect(schemaGate).toMatchObject({
584
+ severity: 'critical',
585
+ blocking: true,
586
+ });
587
+ expect(schemaGate.issue).toContain('person_aliases');
588
+ expect(schemaGate.issue).toContain('meeting_track');
589
+ expect(schemaGate.issue).toContain('series_id');
590
+ expect(attributionGate).toMatchObject({
591
+ severity: 'planning_hint',
592
+ blocking: false,
593
+ });
594
+ expect(synthesis.rewriteRequired).toBe(true);
595
+ });
448
596
  });
449
597
 
450
598
  function makeProject() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/ops-framework",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ops-agent": "bin/ops-agent.mjs"