@besales/ops-framework 0.1.15 → 0.1.17

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.17
4
+
5
+ - 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.
6
+ - Expanded the schema gate action to call out `person_aliases` or alias storage plus required `meetings.*` fields for track classification and speaker resolution.
7
+
8
+ ## 0.1.16
9
+
10
+ - 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.
11
+ - 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.
12
+
3
13
  ## 0.1.15
4
14
 
5
15
  - 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.
@@ -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;
@@ -1100,6 +1102,83 @@ function addApplyContractGate({ gates, sourceText, requirementText }) {
1100
1102
  }
1101
1103
  }
1102
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) {
1130
+ const schemaShape = schemaLooksClosed ? 'closed/minimal schema list' : 'schema/foundation contract';
1131
+ gates.push({
1132
+ id: 'schema-supports-derived-requirements',
1133
+ severity: 'critical',
1134
+ blocking: true,
1135
+ title: 'Schema requirement does not support dependent requirements',
1136
+ issue: `Schema requirement ${schemaReq.id} is written as a ${schemaShape} but omits ${missing.join(', ')}.`,
1137
+ 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. For meeting extraction this usually means `person_aliases` or documented alias storage plus `meetings.meeting_track`, `meetings.series_id`, `meetings.asr_quality`, `meetings.speaker_resolution_status` and `meetings.raw_speaker_labels` when those behaviors are approved.',
1138
+ });
1139
+ }
1140
+ }
1141
+
1142
+ function addSchemaAttributionWarningGate({ gates, sourceText, requirements }) {
1143
+ const schemaReq = findSchemaRequirement(requirements);
1144
+ if (!schemaReq) {
1145
+ return;
1146
+ }
1147
+ const schemaText = requirementFullText(schemaReq);
1148
+ const sourceMentionsAttribution = /(created_by|app\.current_user_id|owner_id|entity_history|enforcement[- ]deferred)/i.test(sourceText);
1149
+ const missing = [
1150
+ ['created_by', /created_by/i],
1151
+ ['app.current_user_id', /app\.current_user_id/i],
1152
+ ['owner_id', /owner_id/i],
1153
+ ['entity_history', /entity_history/i],
1154
+ ['enforcement-deferred', /enforcement[- ]deferred/i],
1155
+ ].filter(([, pattern]) => !pattern.test(schemaText)).map(([name]) => name);
1156
+ if (sourceMentionsAttribution && missing.length) {
1157
+ gates.push({
1158
+ id: 'schema-attribution-nfr-warning',
1159
+ severity: 'planning_hint',
1160
+ blocking: false,
1161
+ title: 'Schema attribution/access fields are not explicit',
1162
+ issue: `Sources mention attribution/access schema decisions, but schema requirement ${schemaReq.id} omits ${missing.join(', ')}.`,
1163
+ 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.',
1164
+ });
1165
+ }
1166
+ }
1167
+
1168
+ function findSchemaRequirement(requirements) {
1169
+ return requirements.find((req) => /(schema|migration|tables?|таблиц|схем)/i.test(requirementFullText(req)));
1170
+ }
1171
+
1172
+ function requirementFullText(req) {
1173
+ return [
1174
+ req.id,
1175
+ req.text,
1176
+ req.acceptance,
1177
+ req.notes,
1178
+ req.workPackage,
1179
+ ].filter(Boolean).join('\n');
1180
+ }
1181
+
1103
1182
  function addSequencingGate({ gates, sourceText, requirementText }) {
1104
1183
  const hasGoldenSet = /(golden set|golden-set|regression baseline|baseline regression)/i.test(`${sourceText}\n${requirementText}`);
1105
1184
  const hasMeetingExtraction = /(process-meeting|process meeting|\/process-meeting|meeting extraction)/i.test(requirementText);
@@ -497,6 +497,188 @@ describe('initiative framework', () => {
497
497
  expect(gate.action).toContain('client+internal');
498
498
  expect(gate.action).toContain('related_person_id');
499
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
+ });
596
+
597
+ it('blocks planning when a non-closed schema foundation omits speaker and meeting fields', () => {
598
+ const root = makeProject();
599
+ const docsDir = path.join(root, 'docs', 'delivery-os');
600
+ fs.mkdirSync(docsDir, { recursive: true });
601
+ fs.writeFileSync(path.join(docsDir, '06-roadmap.md'), [
602
+ '# Phase 1',
603
+ '',
604
+ '- Migration 001b: person_aliases. Meeting fields: meeting_track, series_id, asr_quality, speaker_resolution_status, raw_speaker_labels.',
605
+ ].join('\n'));
606
+ createInitiative({
607
+ projectRoot: root,
608
+ initiativeId: 'delivery-os-mvp',
609
+ title: 'Delivery OS MVP',
610
+ });
611
+ initiativeIntake({
612
+ projectRoot: root,
613
+ initiativeId: 'delivery-os-mvp',
614
+ docsDir: 'docs/delivery-os',
615
+ phase: 'phase-1',
616
+ });
617
+ const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
618
+ fs.writeFileSync(path.join(initiativeDir, 'requirements-map.md'), [
619
+ '# Requirements Map',
620
+ '',
621
+ '## Requirement Details',
622
+ '',
623
+ '### REQ-002',
624
+ '',
625
+ '- Status: `approved`',
626
+ '- Source: `docs/delivery-os/04-data-model.md:1206`',
627
+ '- Source hash: `sha`',
628
+ '- Phase: `phase-1`',
629
+ '- Quality: `complete`',
630
+ '- Quality issue: (none)',
631
+ '- Work package: WP-001',
632
+ '- Acceptance: Schema includes approved Phase 1 entities plus attribution/access foundations: created_by, app.current_user_id, owner_id, entity_history, enforcement-deferred.',
633
+ '- Decision: `approve`',
634
+ '- Notes: Human-approved attribution update.',
635
+ '- Requirement: Minimal Phase 1 Schema And Attribution Foundations.',
636
+ '',
637
+ '### REQ-003',
638
+ '',
639
+ '- Status: `approved`',
640
+ '- Source: `docs/delivery-os/06-roadmap.md:10`',
641
+ '- Source hash: `sha`',
642
+ '- Phase: `phase-1`',
643
+ '- Quality: `complete`',
644
+ '- Quality issue: (none)',
645
+ '- Work package: WP-003',
646
+ '- Acceptance: Meetings are classified with meeting_track.',
647
+ '- Decision: `approve`',
648
+ '- Notes: (empty)',
649
+ '- Requirement: Phase 1 classification uses meeting_track for client/internal/tracker/advisor_network/investor/grant_funding.',
650
+ '',
651
+ '### REQ-004',
652
+ '',
653
+ '- Status: `approved`',
654
+ '- Source: `docs/delivery-os/13-meeting-extraction-profiles.md:240`',
655
+ '- Source hash: `sha`',
656
+ '- Phase: `phase-1`',
657
+ '- Quality: `complete`',
658
+ '- Quality issue: (none)',
659
+ '- Work package: WP-001, WP-003',
660
+ '- Acceptance: Speaker resolution stores raw speaker labels/aliases and speaker_resolution_status.',
661
+ '- Decision: `approve`',
662
+ '- Notes: (empty)',
663
+ '- Requirement: Phase 1 supports speaker identity resolution and alias storage.',
664
+ '',
665
+ ].join('\n'));
666
+
667
+ const synthesis = initiativeRequirementsSynthesis({
668
+ projectRoot: root,
669
+ initiativeId: 'delivery-os-mvp',
670
+ });
671
+
672
+ const schemaGate = synthesis.gates.find((gate) => gate.id === 'schema-supports-derived-requirements');
673
+ expect(schemaGate).toMatchObject({
674
+ severity: 'critical',
675
+ blocking: true,
676
+ });
677
+ expect(schemaGate.issue).toContain('schema/foundation contract');
678
+ expect(schemaGate.issue).toContain('person_aliases');
679
+ expect(schemaGate.issue).toContain('meeting_track');
680
+ expect(schemaGate.action).toContain('meetings.raw_speaker_labels');
681
+ });
500
682
  });
501
683
 
502
684
  function makeProject() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/ops-framework",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ops-agent": "bin/ops-agent.mjs"