@besales/ops-framework 0.1.13 → 0.1.15
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 +10 -0
- package/README.md +1 -1
- package/bin/initiative.mjs +169 -2
- package/bin/initiative.test.mjs +122 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.15
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Linked the track-scope gate action to the apply-contract decision so synthesis resolves both scope and write semantics together.
|
|
7
|
+
|
|
8
|
+
## 0.1.14
|
|
9
|
+
|
|
10
|
+
- 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.
|
|
11
|
+
- Made `initiative-requirements-synthesis` block planning when source docs contain important concepts that synthesized requirements do not explicitly cover.
|
|
12
|
+
|
|
3
13
|
## 0.1.13
|
|
4
14
|
|
|
5
15
|
- 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,154 @@ 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 combinedText = `${requirementText}\n${sourceText}`;
|
|
1068
|
+
const mentionsWideTrackTaxonomy = /(tracker|advisor[_ -]?network|advisor-network|investor|grant[_ -]?funding|fundraising|grants?)/i.test(combinedText);
|
|
1069
|
+
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)
|
|
1070
|
+
|| /(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);
|
|
1071
|
+
const defersFactStorage = /(commitments?|decisions?|risks?|related_person_id).{0,160}(phase\s*2|future|defer|deferred|отлож)/is.test(combinedText)
|
|
1072
|
+
|| /(phase\s*2|future|defer|deferred|отлож).{0,160}(commitments?|decisions?|risks?|related_person_id)/is.test(combinedText);
|
|
1073
|
+
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);
|
|
1074
|
+
if (mentionsWideTrackTaxonomy && mapsWideTracksToFactStorage && defersFactStorage && !hasExplicitTrackScopeDecision) {
|
|
1075
|
+
gates.push({
|
|
1076
|
+
id: 'scope-storage-track-conflict',
|
|
1077
|
+
severity: 'needs_decision',
|
|
1078
|
+
blocking: true,
|
|
1079
|
+
title: 'Track taxonomy conflicts with deferred fact storage',
|
|
1080
|
+
issue: 'Sources connect non-client/internal meeting tracks to commitments/decisions/risks or related-person facts while that storage appears deferred.',
|
|
1081
|
+
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.',
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function addApplyContractGate({ gates, sourceText, requirementText }) {
|
|
1087
|
+
const hasApplyFlow = /(apply|approval|proposedchange|proposed change|process-meeting|process meeting|\/process-meeting)/i.test(requirementText);
|
|
1088
|
+
const hasDeferredFacts = /(commitments?|decisions?|risks?|projects?).{0,120}(phase\s*2|future|defer|отлож)/is.test(`${requirementText}\n${sourceText}`)
|
|
1089
|
+
|| /(phase\s*2|future|defer|отлож).{0,120}(commitments?|decisions?|risks?|projects?)/is.test(`${requirementText}\n${sourceText}`);
|
|
1090
|
+
const definesDraftOnlyContract = /(draft|proposed only|not applied|не применяется|чернов|только proposed|only proposed)/i.test(requirementText);
|
|
1091
|
+
if (hasApplyFlow && hasDeferredFacts && !definesDraftOnlyContract) {
|
|
1092
|
+
gates.push({
|
|
1093
|
+
id: 'apply-contract-ambiguous',
|
|
1094
|
+
severity: 'needs_decision',
|
|
1095
|
+
blocking: true,
|
|
1096
|
+
title: 'Phase apply contract is ambiguous',
|
|
1097
|
+
issue: 'Apply/approval flow exists, but some extracted fact domains are deferred. Requirements do not say what is applied versus kept as draft/proposed.',
|
|
1098
|
+
action: 'State acceptance for Phase 1 apply: which entities are written, which outputs remain draft ProposedChange, and which domains are out of scope.',
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function addSequencingGate({ gates, sourceText, requirementText }) {
|
|
1104
|
+
const hasGoldenSet = /(golden set|golden-set|regression baseline|baseline regression)/i.test(`${sourceText}\n${requirementText}`);
|
|
1105
|
+
const hasMeetingExtraction = /(process-meeting|process meeting|\/process-meeting|meeting extraction)/i.test(requirementText);
|
|
1106
|
+
const hasOrdering = /(golden set|golden-set).{0,120}(before|parallel|перед|раньше|параллель)/is.test(requirementText)
|
|
1107
|
+
|| /(before|parallel|перед|раньше|параллель).{0,120}(golden set|golden-set)/is.test(requirementText);
|
|
1108
|
+
if (hasGoldenSet && hasMeetingExtraction && !hasOrdering) {
|
|
1109
|
+
gates.push({
|
|
1110
|
+
id: 'sequencing-golden-set-before-extraction',
|
|
1111
|
+
severity: 'planning_hint',
|
|
1112
|
+
blocking: false,
|
|
1113
|
+
title: 'Golden set should baseline meeting extraction',
|
|
1114
|
+
issue: 'Sources mention golden set/regression baseline and requirements mention meeting extraction, but sequencing is not explicit.',
|
|
1115
|
+
action: 'Plan golden set before or parallel to /process-meeting implementation so quality has a baseline.',
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function addStrongerSourceRefGate({ gates, sourceDocs, requirements }) {
|
|
1121
|
+
const feedbackSources = sourceDocs.filter((source) => /feedback/i.test(source.path));
|
|
1122
|
+
if (!feedbackSources.length) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const feedbackText = feedbackSources.map((source) => source.content).join('\n');
|
|
1126
|
+
for (const req of requirements) {
|
|
1127
|
+
if (/backfill/i.test(req.text) && /backfill/i.test(feedbackText) && !/feedback/i.test(req.source)) {
|
|
1128
|
+
gates.push({
|
|
1129
|
+
id: `stronger-source-ref-${req.id}`,
|
|
1130
|
+
severity: 'traceability',
|
|
1131
|
+
blocking: false,
|
|
1132
|
+
title: `Stronger source reference available for ${req.id}`,
|
|
1133
|
+
issue: 'Requirement mentions backfill, and feedback docs also document backfill decisions, but the requirement source is not a feedback log.',
|
|
1134
|
+
action: 'Add feedback-log source refs or derived-from references before approval.',
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions, questions, gates = [], rewriteRequired }) {
|
|
991
1141
|
const rewriteCandidates = requirements.filter((req) => requirementNeedsRewrite(req));
|
|
1142
|
+
const blockingGates = gates.filter((gate) => gate.blocking);
|
|
992
1143
|
return [
|
|
993
1144
|
'# Requirements Synthesis Pack',
|
|
994
1145
|
'',
|
|
@@ -996,6 +1147,7 @@ function renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions
|
|
|
996
1147
|
`- Requirement candidates: ${requirements.length}`,
|
|
997
1148
|
`- Candidates needing rewrite: ${rewriteCandidates.length}`,
|
|
998
1149
|
`- Relevant sources still missing: ${sourceCoverage?.relevantExcludedSources?.length || 0}`,
|
|
1150
|
+
`- Blocking readiness gates: ${blockingGates.length}`,
|
|
999
1151
|
'',
|
|
1000
1152
|
'## Source Coverage Gate',
|
|
1001
1153
|
'',
|
|
@@ -1009,6 +1161,21 @@ function renderRequirementsSynthesis({ sourceCoverage, requirements, assumptions
|
|
|
1009
1161
|
? rewriteCandidates.map((req) => `- ${req.id} (${req.quality}): ${req.qualityIssue || 'Rewrite into a clean requirement with acceptance.'}`)
|
|
1010
1162
|
: ['- None.']),
|
|
1011
1163
|
'',
|
|
1164
|
+
'## Synthesis Readiness Gates',
|
|
1165
|
+
'',
|
|
1166
|
+
...(gates.length
|
|
1167
|
+
? gates.map((gate) => [
|
|
1168
|
+
`### ${gate.id}`,
|
|
1169
|
+
'',
|
|
1170
|
+
`- Severity: \`${gate.severity}\``,
|
|
1171
|
+
`- Blocking: \`${gate.blocking ? 'yes' : 'no'}\``,
|
|
1172
|
+
`- Title: ${gate.title}`,
|
|
1173
|
+
`- Issue: ${gate.issue}`,
|
|
1174
|
+
`- Required action: ${gate.action}`,
|
|
1175
|
+
'',
|
|
1176
|
+
].join('\n'))
|
|
1177
|
+
: ['- None.']),
|
|
1178
|
+
'',
|
|
1012
1179
|
'## Synthesis Instructions',
|
|
1013
1180
|
'',
|
|
1014
1181
|
'Create final requirements only after human/LLM synthesis. Each final requirement must have:',
|
package/bin/initiative.test.mjs
CHANGED
|
@@ -375,6 +375,128 @@ 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
|
+
'- advisor_network, fundraising and grants are routed into commitments, decisions and risks by meeting_track.',
|
|
392
|
+
'- Phase 2 defers commitments decisions risks related_person_id storage.',
|
|
393
|
+
].join('\n'));
|
|
394
|
+
fs.writeFileSync(path.join(docsDir, '07-adrs.md'), [
|
|
395
|
+
'# ADR-6',
|
|
396
|
+
'',
|
|
397
|
+
'- Schema-ready enforcement-deferred: created_by, app.current_user_id, owner_id, entity_history.',
|
|
398
|
+
].join('\n'));
|
|
399
|
+
fs.writeFileSync(path.join(docsDir, '05-skills-catalog.md'), [
|
|
400
|
+
'# Skills',
|
|
401
|
+
'',
|
|
402
|
+
'- Backfill must stay split from Phase 1 rollout.',
|
|
403
|
+
].join('\n'));
|
|
404
|
+
fs.writeFileSync(path.join(docsDir, '99-feedback-log.md'), [
|
|
405
|
+
'# Feedback',
|
|
406
|
+
'',
|
|
407
|
+
'- Decision: backfill split is documented here with the stronger source reference.',
|
|
408
|
+
].join('\n'));
|
|
409
|
+
createInitiative({
|
|
410
|
+
projectRoot: root,
|
|
411
|
+
initiativeId: 'delivery-os-mvp',
|
|
412
|
+
title: 'Delivery OS MVP',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
initiativeIntake({
|
|
416
|
+
projectRoot: root,
|
|
417
|
+
initiativeId: 'delivery-os-mvp',
|
|
418
|
+
docsDir: 'docs/delivery-os',
|
|
419
|
+
phase: 'phase-1',
|
|
420
|
+
});
|
|
421
|
+
initiativeRequirements({
|
|
422
|
+
projectRoot: root,
|
|
423
|
+
initiativeId: 'delivery-os-mvp',
|
|
424
|
+
phase: 'phase-1',
|
|
425
|
+
});
|
|
426
|
+
const synthesis = initiativeRequirementsSynthesis({
|
|
427
|
+
projectRoot: root,
|
|
428
|
+
initiativeId: 'delivery-os-mvp',
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const gateIds = synthesis.gates.map((gate) => gate.id);
|
|
432
|
+
expect(synthesis.rewriteRequired).toBe(true);
|
|
433
|
+
expect(gateIds).toContain('critical-concept-speaker-resolution');
|
|
434
|
+
expect(gateIds).toContain('nfr-attribution-audit-access');
|
|
435
|
+
expect(gateIds).toContain('scope-storage-track-conflict');
|
|
436
|
+
expect(gateIds).toContain('apply-contract-ambiguous');
|
|
437
|
+
expect(gateIds).toContain('sequencing-golden-set-before-extraction');
|
|
438
|
+
expect(gateIds.some((id) => id.startsWith('stronger-source-ref-'))).toBe(true);
|
|
439
|
+
|
|
440
|
+
const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
|
|
441
|
+
const pack = fs.readFileSync(path.join(initiativeDir, 'requirements-synthesis.md'), 'utf8');
|
|
442
|
+
expect(pack).toContain('Synthesis Readiness Gates');
|
|
443
|
+
expect(pack).toContain('Speaker identity resolution and person aliases');
|
|
444
|
+
expect(pack).toContain('Track taxonomy conflicts with deferred fact storage');
|
|
445
|
+
expect(pack).toContain('Phase apply contract is ambiguous');
|
|
446
|
+
expect(pack).toContain('Golden set should baseline meeting extraction');
|
|
447
|
+
expect(pack).toContain('Stronger source reference available');
|
|
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
|
+
});
|
|
378
500
|
});
|
|
379
501
|
|
|
380
502
|
function makeProject() {
|