openplanr 3.5.0 → 3.6.0
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 +31 -0
- package/dist/cli/commands/backlog-commands.d.ts +3 -0
- package/dist/cli/commands/backlog-commands.d.ts.map +1 -0
- package/dist/cli/commands/backlog-commands.js +60 -0
- package/dist/cli/commands/backlog-commands.js.map +1 -0
- package/dist/cli/commands/company.d.ts.map +1 -1
- package/dist/cli/commands/company.js +17 -0
- package/dist/cli/commands/company.js.map +1 -1
- package/dist/cli/commands/planning-artifacts.d.ts.map +1 -1
- package/dist/cli/commands/planning-artifacts.js +13 -5
- package/dist/cli/commands/planning-artifacts.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-B-l_941W.js → index-C_OquZVk.js} +2 -2
- package/dist/dashboard/assets/{index-B-l_941W.js.map → index-C_OquZVk.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +8 -8
- package/dist/dashboard/index.html +1 -1
- package/dist/services/artifact-service.d.ts.map +1 -1
- package/dist/services/artifact-service.js +13 -6
- package/dist/services/artifact-service.js.map +1 -1
- package/dist/services/company-export-service.d.ts +41 -0
- package/dist/services/company-export-service.d.ts.map +1 -0
- package/dist/services/company-export-service.js +429 -0
- package/dist/services/company-export-service.js.map +1 -0
- package/dist/services/company-sync-service.d.ts +24 -1
- package/dist/services/company-sync-service.d.ts.map +1 -1
- package/dist/services/company-sync-service.js +36 -19
- package/dist/services/company-sync-service.js.map +1 -1
- package/docs/CLI.md +97 -1
- package/lib/backlog-item.d.mts +45 -1
- package/lib/backlog-item.mjs +302 -34
- package/lib/host-packages/adapter-registry.json +4 -4
- package/lib/host-packages/capability-map.json +1 -1
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/claude/openplanr/.claude-plugin/icon.png +0 -0
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +77 -77
- package/lib/host-packages/claude/openplanr/README.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/ceo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/ceo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/claude/openplanr/skills/chair-review/SKILL.md +2 -7
- package/lib/host-packages/claude/openplanr/skills/chair-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/challenger-review/SKILL.md +3 -7
- package/lib/host-packages/claude/openplanr/skills/challenger-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/cmo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/cmo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/claude/openplanr/skills/coo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/coo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/claude/openplanr/skills/cpo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/cpo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/claude/openplanr/skills/cto-review/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/cto-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/claude/openplanr/skills/investigate/SKILL.md +2 -3
- package/lib/host-packages/claude/openplanr/skills/investigate/references/backlog-filing.md +102 -0
- package/lib/host-packages/claude/openplanr/skills/investigate/references/findings.md +8 -16
- package/lib/host-packages/claude/openplanr/skills/operate/SKILL.md +73 -84
- package/lib/host-packages/claude/openplanr/skills/operate/references/note-checklist.md +131 -0
- package/lib/host-packages/claude/openplanr/skills/operate/references/return-contract.md +10 -0
- package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +16 -17
- package/lib/host-packages/claude/openplanr/skills/plan/references/planning-ids.md +53 -0
- package/lib/host-packages/claude/openplanr/skills/plan/references/return-contract.md +10 -0
- package/lib/host-packages/claude/openplanr/skills/plan-review/SKILL.md +6 -6
- package/lib/host-packages/claude/openplanr/skills/plan-review/references/return-contract.md +10 -0
- package/lib/host-packages/claude/openplanr/skills/release/SKILL.md +40 -139
- package/lib/host-packages/claude/openplanr/skills/release/references/cadence.md +10 -0
- package/lib/host-packages/claude/openplanr/skills/release/references/release-notes.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/release/references/return-contract.md +10 -0
- package/lib/host-packages/claude/openplanr/skills/release/references/schemes.md +4 -2
- package/lib/host-packages/claude/openplanr/skills/ship/SKILL.md +37 -24
- package/lib/host-packages/claude/openplanr/skills/ship/references/backlog-filing.md +102 -0
- package/lib/host-packages/claude/openplanr/skills/ship/references/findings.md +8 -16
- package/lib/host-packages/claude/openplanr/skills/ship/references/return-contract.md +10 -0
- package/lib/host-packages/claude/openplanr/skills/spec/SKILL.md +9 -9
- package/lib/host-packages/claude/openplanr/skills/spec/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +74 -74
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-ceo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-ceo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-chair-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-chair-review.mdc +2 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-challenger-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-challenger-review.mdc +3 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-cmo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-cmo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-coo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-coo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-cpo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-cpo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-cto-review/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-cto-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-investigate/references/backlog-filing.md +102 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-investigate/references/findings.md +8 -16
- package/lib/host-packages/cursor/openplanr/rules/planr-investigate.mdc +2 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-operate/references/note-checklist.md +131 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-operate/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-operate.mdc +73 -84
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/planning-ids.md +53 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan-review/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan-review.mdc +6 -6
- package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +16 -17
- package/lib/host-packages/cursor/openplanr/rules/planr-release/references/cadence.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-release/references/release-notes.md +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-release/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-release/references/schemes.md +4 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-release.mdc +40 -139
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/backlog-filing.md +102 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/findings.md +8 -16
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-ship.mdc +37 -24
- package/lib/host-packages/cursor/openplanr/rules/planr-spec/references/return-contract.md +10 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-spec.mdc +9 -9
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +89 -89
- package/lib/host-packages/openai/openplanr/skills/ceo-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/ceo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/ceo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/openai/openplanr/skills/chair-review/SKILL.md +2 -7
- package/lib/host-packages/openai/openplanr/skills/chair-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/chair-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/challenger-review/SKILL.md +3 -7
- package/lib/host-packages/openai/openplanr/skills/challenger-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/challenger-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/cmo-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/cmo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/cmo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/openai/openplanr/skills/coo-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/coo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/coo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/openai/openplanr/skills/cpo-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/cpo-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/cpo-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/openai/openplanr/skills/cto-review/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/cto-review/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/cto-review/references/operate-advisor-contract.md +2 -7
- package/lib/host-packages/openai/openplanr/skills/investigate/SKILL.md +2 -3
- package/lib/host-packages/openai/openplanr/skills/investigate/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/investigate/references/backlog-filing.md +102 -0
- package/lib/host-packages/openai/openplanr/skills/investigate/references/findings.md +8 -16
- package/lib/host-packages/openai/openplanr/skills/operate/SKILL.md +73 -84
- package/lib/host-packages/openai/openplanr/skills/operate/openplanr.skill.json +15 -4
- package/lib/host-packages/openai/openplanr/skills/operate/references/note-checklist.md +131 -0
- package/lib/host-packages/openai/openplanr/skills/operate/references/return-contract.md +10 -0
- package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +16 -17
- package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +15 -4
- package/lib/host-packages/openai/openplanr/skills/plan/references/planning-ids.md +53 -0
- package/lib/host-packages/openai/openplanr/skills/plan/references/return-contract.md +10 -0
- package/lib/host-packages/openai/openplanr/skills/plan-review/SKILL.md +6 -6
- package/lib/host-packages/openai/openplanr/skills/plan-review/openplanr.skill.json +12 -1
- package/lib/host-packages/openai/openplanr/skills/plan-review/references/return-contract.md +10 -0
- package/lib/host-packages/openai/openplanr/skills/release/SKILL.md +40 -139
- package/lib/host-packages/openai/openplanr/skills/release/openplanr.skill.json +4 -4
- package/lib/host-packages/openai/openplanr/skills/release/references/cadence.md +10 -0
- package/lib/host-packages/openai/openplanr/skills/release/references/release-notes.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/release/references/return-contract.md +10 -0
- package/lib/host-packages/openai/openplanr/skills/release/references/schemes.md +4 -2
- package/lib/host-packages/openai/openplanr/skills/ship/SKILL.md +37 -24
- package/lib/host-packages/openai/openplanr/skills/ship/openplanr.skill.json +9 -9
- package/lib/host-packages/openai/openplanr/skills/ship/references/backlog-filing.md +102 -0
- package/lib/host-packages/openai/openplanr/skills/ship/references/findings.md +8 -16
- package/lib/host-packages/openai/openplanr/skills/ship/references/return-contract.md +10 -0
- package/lib/host-packages/openai/openplanr/skills/spec/SKILL.md +9 -9
- package/lib/host-packages/openai/openplanr/skills/spec/openplanr.skill.json +12 -1
- package/lib/host-packages/openai/openplanr/skills/spec/references/return-contract.md +10 -0
- package/package.json +1 -1
- package/lib/host-packages/claude/openplanr/.claude-plugin/icon.svg +0 -11
- package/lib/host-packages/claude/openplanr/runtime/operate/scripts/errors.mjs +0 -8
- package/lib/host-packages/claude/openplanr/runtime/operate/scripts/operate-review-contract.mjs +0 -428
- package/lib/host-packages/claude/openplanr/runtime/operate/scripts/operate-review-note.mjs +0 -686
- package/lib/host-packages/claude/openplanr/runtime/operate/scripts/validate-note.mjs +0 -39
- package/lib/host-packages/claude/openplanr/skills/ceo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/chair-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/challenger-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/cmo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/coo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/cpo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/cto-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/investigate/scripts/backlog-item.mjs +0 -339
- package/lib/host-packages/claude/openplanr/skills/operate/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/planning-ids.mjs +0 -166
- package/lib/host-packages/claude/openplanr/skills/release/scripts/collect_changes.sh +0 -85
- package/lib/host-packages/claude/openplanr/skills/ship/scripts/backlog-item.mjs +0 -339
- package/lib/host-packages/claude/openplanr/skills/ship/scripts/discover-verification.mjs +0 -108
- package/lib/host-packages/cursor/openplanr/rules/planr-ceo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-chair-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-challenger-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-cmo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-coo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-cpo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-cto-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-investigate/scripts/backlog-item.mjs +0 -339
- package/lib/host-packages/cursor/openplanr/rules/planr-operate/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/planning-ids.mjs +0 -166
- package/lib/host-packages/cursor/openplanr/rules/planr-release/scripts/collect_changes.sh +0 -85
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/scripts/backlog-item.mjs +0 -339
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/scripts/discover-verification.mjs +0 -108
- package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/errors.mjs +0 -8
- package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/operate-review-contract.mjs +0 -428
- package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/operate-review-note.mjs +0 -686
- package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/validate-note.mjs +0 -39
- package/lib/host-packages/openai/openplanr/runtime/operate/scripts/errors.mjs +0 -8
- package/lib/host-packages/openai/openplanr/runtime/operate/scripts/operate-review-contract.mjs +0 -428
- package/lib/host-packages/openai/openplanr/runtime/operate/scripts/operate-review-note.mjs +0 -686
- package/lib/host-packages/openai/openplanr/runtime/operate/scripts/validate-note.mjs +0 -39
- package/lib/host-packages/openai/openplanr/skills/ceo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/chair-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/challenger-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/cmo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/coo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/cpo-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/cto-review/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/investigate/scripts/backlog-item.mjs +0 -339
- package/lib/host-packages/openai/openplanr/skills/operate/scripts/validate-note.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/planning-ids.mjs +0 -166
- package/lib/host-packages/openai/openplanr/skills/release/scripts/collect_changes.sh +0 -85
- package/lib/host-packages/openai/openplanr/skills/ship/scripts/backlog-item.mjs +0 -339
- package/lib/host-packages/openai/openplanr/skills/ship/scripts/discover-verification.mjs +0 -108
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
|
|
3
|
-
import { SkillRuntimeError } from './errors.mjs';
|
|
4
|
-
import {
|
|
5
|
-
OPERATE_REVIEW_CONTRACT_VERSION,
|
|
6
|
-
OPERATE_REVIEW_CONTRACT_VERSIONS,
|
|
7
|
-
OPERATE_REVIEW_CONTRACTS,
|
|
8
|
-
} from './operate-review-contract.mjs';
|
|
9
|
-
|
|
10
|
-
const canonicalText = (value) => String(value).replace(/\r\n/gu, '\n');
|
|
11
|
-
const normalizeHeading = (value) => value.trim().replace(/\s+/gu, ' ');
|
|
12
|
-
const PLACEHOLDER = /^(?:<[^>]+>|\.{3}|tbd|todo)$/iu;
|
|
13
|
-
const CONTRACT_DECLARATION =
|
|
14
|
-
/^(?:>|-)\s+\*\*Contract:\*\*\s*([a-z][a-z0-9-]*)@(\d+\.\d+\.\d+)\s*$/gmu;
|
|
15
|
-
const AUTO_CONTRACT_VERSION = 'auto';
|
|
16
|
-
|
|
17
|
-
function diagnostic(code, message, details = {}) {
|
|
18
|
-
return Object.freeze({ code, message, ...details });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function contractFor(version) {
|
|
22
|
-
const contract = OPERATE_REVIEW_CONTRACTS[version];
|
|
23
|
-
if (!contract) {
|
|
24
|
-
throw new SkillRuntimeError(
|
|
25
|
-
'E_OPERATE_REVIEW_CONTRACT_VERSION_INVALID',
|
|
26
|
-
`Unknown Operate review-note contract version ${String(version)}.`,
|
|
27
|
-
{ version, versions: OPERATE_REVIEW_CONTRACT_VERSIONS },
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return contract;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function declaredContract(markdown) {
|
|
34
|
-
const declarations = [...markdown.matchAll(CONTRACT_DECLARATION)].map((match) => ({
|
|
35
|
-
kind: match[1],
|
|
36
|
-
version: match[2],
|
|
37
|
-
}));
|
|
38
|
-
if (declarations.length > 1) {
|
|
39
|
-
throw new SkillRuntimeError(
|
|
40
|
-
'E_OPERATE_REVIEW_CONTRACT_DECLARATION_DUPLICATE',
|
|
41
|
-
'Operate review note declares more than one contract version.',
|
|
42
|
-
{ declarations },
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (declarations.length === 0) return null;
|
|
46
|
-
const declaration = declarations[0];
|
|
47
|
-
const contract = OPERATE_REVIEW_CONTRACTS[declaration.version];
|
|
48
|
-
if (!contract || contract.kind !== declaration.kind) {
|
|
49
|
-
throw new SkillRuntimeError(
|
|
50
|
-
'E_OPERATE_REVIEW_CONTRACT_DECLARATION_INVALID',
|
|
51
|
-
`Unsupported Operate review-note contract ${declaration.kind}@${declaration.version}.`,
|
|
52
|
-
{ declaration, versions: OPERATE_REVIEW_CONTRACT_VERSIONS },
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
return Object.freeze(declaration);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function sectionIndex(markdown) {
|
|
59
|
-
const headings = [...markdown.matchAll(/^##\s+(.+?)\s*$/gmu)].map((match) => ({
|
|
60
|
-
name: normalizeHeading(match[1]),
|
|
61
|
-
start: match.index,
|
|
62
|
-
bodyStart: match.index + match[0].length,
|
|
63
|
-
}));
|
|
64
|
-
return headings.map((heading, index) =>
|
|
65
|
-
Object.freeze({
|
|
66
|
-
...heading,
|
|
67
|
-
body: markdown.slice(heading.bodyStart, headings[index + 1]?.start ?? markdown.length).trim(),
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function profileFor(contract, profile) {
|
|
73
|
-
const profileContract = contract.profiles[profile];
|
|
74
|
-
if (!profileContract) {
|
|
75
|
-
throw new SkillRuntimeError(
|
|
76
|
-
'E_OPERATE_REVIEW_PROFILE_INVALID',
|
|
77
|
-
`Unknown Operate review-note profile ${String(profile)}.`,
|
|
78
|
-
{ profile, profiles: Object.keys(contract.profiles) },
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
return profileContract;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Identify the contract used by a current or historical Operate Markdown note. */
|
|
85
|
-
export function detectOperateReviewNoteContract(
|
|
86
|
-
markdown,
|
|
87
|
-
{ profile, contractVersion = AUTO_CONTRACT_VERSION } = {},
|
|
88
|
-
) {
|
|
89
|
-
if (
|
|
90
|
-
contractVersion !== AUTO_CONTRACT_VERSION &&
|
|
91
|
-
!OPERATE_REVIEW_CONTRACT_VERSIONS.includes(contractVersion)
|
|
92
|
-
) {
|
|
93
|
-
contractFor(contractVersion);
|
|
94
|
-
}
|
|
95
|
-
const source = canonicalText(markdown);
|
|
96
|
-
const firstSection = source.search(/^##\s+/mu);
|
|
97
|
-
const preamble = source.slice(0, firstSection === -1 ? source.length : firstSection);
|
|
98
|
-
const declaration = declaredContract(preamble);
|
|
99
|
-
if (contractVersion !== AUTO_CONTRACT_VERSION) {
|
|
100
|
-
const contract = contractFor(contractVersion);
|
|
101
|
-
profileFor(contract, profile);
|
|
102
|
-
return Object.freeze({
|
|
103
|
-
contractVersion,
|
|
104
|
-
contractKind: contract.kind,
|
|
105
|
-
versionSource: 'requested',
|
|
106
|
-
declaredContractVersion: declaration?.version ?? null,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
if (declaration) {
|
|
110
|
-
const contract = contractFor(declaration.version);
|
|
111
|
-
profileFor(contract, profile);
|
|
112
|
-
return Object.freeze({
|
|
113
|
-
contractVersion: declaration.version,
|
|
114
|
-
contractKind: contract.kind,
|
|
115
|
-
versionSource: 'declared',
|
|
116
|
-
declaredContractVersion: declaration.version,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const headings = new Set(sectionIndex(source).map(({ name }) => name));
|
|
121
|
-
const scores = OPERATE_REVIEW_CONTRACT_VERSIONS.map((version) => {
|
|
122
|
-
const contract = contractFor(version);
|
|
123
|
-
const profileContract = profileFor(contract, profile);
|
|
124
|
-
return {
|
|
125
|
-
version,
|
|
126
|
-
score: profileContract.sections.filter((section) => headings.has(section)).length,
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
const legacy = scores.find(({ version }) => version === '1.0.0');
|
|
130
|
-
const current = scores.find(({ version }) => version === OPERATE_REVIEW_CONTRACT_VERSION);
|
|
131
|
-
const contractVersionDetected = legacy.score > current.score ? legacy.version : current.version;
|
|
132
|
-
const contract = contractFor(contractVersionDetected);
|
|
133
|
-
return Object.freeze({
|
|
134
|
-
contractVersion: contractVersionDetected,
|
|
135
|
-
contractKind: contract.kind,
|
|
136
|
-
versionSource: Math.max(legacy.score, current.score) > 0 ? 'structure' : 'default',
|
|
137
|
-
declaredContractVersion: null,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function fieldsIn(markdown) {
|
|
142
|
-
const fields = new Map();
|
|
143
|
-
for (const match of markdown.matchAll(/^(?:>|-)\s+\*\*([^*\n]+?):\*\*\s*(.*?)\s*$/gmu)) {
|
|
144
|
-
const name = normalizeHeading(match[1]);
|
|
145
|
-
if (!fields.has(name)) fields.set(name, []);
|
|
146
|
-
fields.get(name).push(match[2]);
|
|
147
|
-
}
|
|
148
|
-
return fields;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function requireFields(markdown, required, diagnostics, context, allowedValues = {}) {
|
|
152
|
-
const fields = fieldsIn(markdown);
|
|
153
|
-
for (const field of required) {
|
|
154
|
-
const values = fields.get(field) ?? [];
|
|
155
|
-
if (
|
|
156
|
-
values.length === 0 ||
|
|
157
|
-
values.every((value) => value.length === 0 || PLACEHOLDER.test(value))
|
|
158
|
-
) {
|
|
159
|
-
diagnostics.push(
|
|
160
|
-
diagnostic(
|
|
161
|
-
'E_OPERATE_REVIEW_FIELD_MISSING',
|
|
162
|
-
`${context} requires a non-empty ${field} field.`,
|
|
163
|
-
{ context, field },
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
} else if (values.length > 1) {
|
|
167
|
-
diagnostics.push(
|
|
168
|
-
diagnostic(
|
|
169
|
-
'E_OPERATE_REVIEW_FIELD_DUPLICATE',
|
|
170
|
-
`${context} contains ${values.length} ${field} fields.`,
|
|
171
|
-
{ context, field, count: values.length },
|
|
172
|
-
),
|
|
173
|
-
);
|
|
174
|
-
} else if (allowedValues[field] && !allowedValues[field].includes(values[0])) {
|
|
175
|
-
diagnostics.push(
|
|
176
|
-
diagnostic(
|
|
177
|
-
'E_OPERATE_REVIEW_FIELD_VALUE_INVALID',
|
|
178
|
-
`${context} has an invalid ${field} value.`,
|
|
179
|
-
{ context, field, expected: allowedValues[field], actual: values[0] },
|
|
180
|
-
),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function validateSections(markdown, contract, diagnostics) {
|
|
187
|
-
const headings = sectionIndex(markdown);
|
|
188
|
-
const expected = contract.sections;
|
|
189
|
-
const actual = headings.map(({ name }) => name);
|
|
190
|
-
for (const section of expected) {
|
|
191
|
-
const count = actual.filter((name) => name === section).length;
|
|
192
|
-
if (count === 0) {
|
|
193
|
-
diagnostics.push(
|
|
194
|
-
diagnostic('E_OPERATE_REVIEW_SECTION_MISSING', `Required section ${section} is missing.`, {
|
|
195
|
-
section,
|
|
196
|
-
}),
|
|
197
|
-
);
|
|
198
|
-
} else if (count > 1) {
|
|
199
|
-
diagnostics.push(
|
|
200
|
-
diagnostic(
|
|
201
|
-
'E_OPERATE_REVIEW_SECTION_DUPLICATE',
|
|
202
|
-
`Required section ${section} appears ${count} times.`,
|
|
203
|
-
{ section, count },
|
|
204
|
-
),
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const unexpected = actual.filter((section) => !expected.includes(section));
|
|
209
|
-
for (const section of unexpected) {
|
|
210
|
-
diagnostics.push(
|
|
211
|
-
diagnostic(
|
|
212
|
-
'E_OPERATE_REVIEW_SECTION_UNEXPECTED',
|
|
213
|
-
`Unexpected top-level section ${section} is not part of this review contract.`,
|
|
214
|
-
{ section },
|
|
215
|
-
),
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
const presentExpected = actual.filter((section) => expected.includes(section));
|
|
219
|
-
const expectedPresentOrder = expected.filter((section) => actual.includes(section));
|
|
220
|
-
if (JSON.stringify(presentExpected) !== JSON.stringify(expectedPresentOrder)) {
|
|
221
|
-
diagnostics.push(
|
|
222
|
-
diagnostic('E_OPERATE_REVIEW_SECTION_ORDER', 'Review sections are not in contract order.', {
|
|
223
|
-
expected: expectedPresentOrder,
|
|
224
|
-
actual: presentExpected,
|
|
225
|
-
}),
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
return new Map(headings.map(({ name, body }) => [name, body]));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function validateItems(sectionBody, itemContract, diagnostics) {
|
|
232
|
-
if (typeof sectionBody !== 'string') return 0;
|
|
233
|
-
const prefix = itemContract.idPrefix;
|
|
234
|
-
const pattern = new RegExp(`^###\\s+(${prefix}[1-9][0-9]*)\\s+—\\s+(.+?)\\s*$`, 'gmu');
|
|
235
|
-
const items = [...sectionBody.matchAll(pattern)].map((match) => ({
|
|
236
|
-
id: match[1],
|
|
237
|
-
headingSuffix: match[2],
|
|
238
|
-
start: match.index,
|
|
239
|
-
bodyStart: match.index + match[0].length,
|
|
240
|
-
}));
|
|
241
|
-
if (items.length === 0) {
|
|
242
|
-
if (!sectionBody.includes(itemContract.emptyText)) {
|
|
243
|
-
diagnostics.push(
|
|
244
|
-
diagnostic(
|
|
245
|
-
'E_OPERATE_REVIEW_ITEM_OR_EMPTY_STATE_MISSING',
|
|
246
|
-
`${itemContract.section} requires an item or the exact empty state.`,
|
|
247
|
-
{ section: itemContract.section, emptyText: itemContract.emptyText },
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
return 0;
|
|
252
|
-
}
|
|
253
|
-
if (items.length > itemContract.maxItems) {
|
|
254
|
-
diagnostics.push(
|
|
255
|
-
diagnostic(
|
|
256
|
-
'E_OPERATE_REVIEW_ITEM_LIMIT',
|
|
257
|
-
`${itemContract.section} has ${items.length} items; at most ${itemContract.maxItems} are allowed.`,
|
|
258
|
-
{ section: itemContract.section, count: items.length, maxItems: itemContract.maxItems },
|
|
259
|
-
),
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
const expectedIds = items.map((_, index) => `${prefix}${index + 1}`);
|
|
263
|
-
const actualIds = items.map(({ id }) => id);
|
|
264
|
-
if (JSON.stringify(actualIds) !== JSON.stringify(expectedIds)) {
|
|
265
|
-
diagnostics.push(
|
|
266
|
-
diagnostic(
|
|
267
|
-
'E_OPERATE_REVIEW_ITEM_SEQUENCE',
|
|
268
|
-
`${itemContract.section} item IDs must be unique and contiguous.`,
|
|
269
|
-
{ section: itemContract.section, expected: expectedIds, actual: actualIds },
|
|
270
|
-
),
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
for (const [index, item] of items.entries()) {
|
|
274
|
-
if (
|
|
275
|
-
itemContract.headingSuffixPattern &&
|
|
276
|
-
!itemContract.headingSuffixPattern.test(item.headingSuffix)
|
|
277
|
-
) {
|
|
278
|
-
diagnostics.push(
|
|
279
|
-
diagnostic(
|
|
280
|
-
'E_OPERATE_REVIEW_ITEM_HEADING_INVALID',
|
|
281
|
-
`${item.id} must declare a contracted priority in its heading.`,
|
|
282
|
-
{ section: itemContract.section, itemId: item.id },
|
|
283
|
-
),
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
const body = sectionBody
|
|
287
|
-
.slice(item.bodyStart, items[index + 1]?.start ?? sectionBody.length)
|
|
288
|
-
.trim();
|
|
289
|
-
requireFields(body, itemContract.fields, diagnostics, item.id, itemContract.fieldValues);
|
|
290
|
-
}
|
|
291
|
-
return items.length;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function validateSectionItemLimits(sectionBodies, limits, diagnostics) {
|
|
295
|
-
for (const [section, maxItems] of Object.entries(limits ?? {})) {
|
|
296
|
-
const body = sectionBodies.get(section);
|
|
297
|
-
if (typeof body !== 'string' || /^None\.\s*$/iu.test(body)) continue;
|
|
298
|
-
const count = [...body.matchAll(/^\s*-\s+\S/gmu)].length;
|
|
299
|
-
if (count > maxItems) {
|
|
300
|
-
diagnostics.push(
|
|
301
|
-
diagnostic(
|
|
302
|
-
'E_OPERATE_REVIEW_SECTION_ITEM_LIMIT',
|
|
303
|
-
`${section} has ${count} items; at most ${maxItems} are allowed.`,
|
|
304
|
-
{ section, count, maxItems },
|
|
305
|
-
),
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function legacyBoardDecisions(sectionBody) {
|
|
312
|
-
if (typeof sectionBody !== 'string') return [];
|
|
313
|
-
const headings = [...sectionBody.matchAll(/^###\s+(D[1-9][0-9]*)\s+—\s+.+?\s*$/gmu)].map(
|
|
314
|
-
(match) => ({
|
|
315
|
-
id: match[1],
|
|
316
|
-
start: match.index,
|
|
317
|
-
bodyStart: match.index + match[0].length,
|
|
318
|
-
}),
|
|
319
|
-
);
|
|
320
|
-
return headings.map((heading, index) => {
|
|
321
|
-
const body = sectionBody
|
|
322
|
-
.slice(heading.bodyStart, headings[index + 1]?.start ?? sectionBody.length)
|
|
323
|
-
.trim();
|
|
324
|
-
return Object.freeze({
|
|
325
|
-
id: heading.id,
|
|
326
|
-
owner: fieldsIn(body).get('Owner')?.[0] ?? null,
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function escapedPattern(value) {
|
|
332
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function validateLegacyDecisionGate(gate, decision, diagnostics) {
|
|
336
|
-
const statements = gate
|
|
337
|
-
.split('\n')
|
|
338
|
-
.map((line) => line.trim())
|
|
339
|
-
.filter((line) => line.length > 0 && new RegExp(`\\b${decision.id}\\b`, 'u').test(line));
|
|
340
|
-
if (statements.length !== 1) {
|
|
341
|
-
diagnostics.push(
|
|
342
|
-
diagnostic(
|
|
343
|
-
'E_OPERATE_REVIEW_HUMAN_GATE_INCOMPLETE',
|
|
344
|
-
`Human gate must contain exactly one disposition statement for ${decision.id}.`,
|
|
345
|
-
{ section: 'Human gate', decisionId: decision.id, statementCount: statements.length },
|
|
346
|
-
),
|
|
347
|
-
);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const statement = statements[0];
|
|
352
|
-
const missing = [];
|
|
353
|
-
for (const disposition of ['adopt', 'reject', 'defer']) {
|
|
354
|
-
if (!new RegExp(`\\b${disposition}\\b`, 'iu').test(statement)) missing.push(disposition);
|
|
355
|
-
}
|
|
356
|
-
if (!decision.owner || !new RegExp(escapedPattern(decision.owner), 'iu').test(statement)) {
|
|
357
|
-
missing.push('named owner');
|
|
358
|
-
}
|
|
359
|
-
if (!/\breason\b/iu.test(statement)) missing.push('reason');
|
|
360
|
-
if (missing.length > 0) {
|
|
361
|
-
diagnostics.push(
|
|
362
|
-
diagnostic(
|
|
363
|
-
'E_OPERATE_REVIEW_HUMAN_GATE_INCOMPLETE',
|
|
364
|
-
`Human gate disposition for ${decision.id} is missing: ${missing.join(', ')}.`,
|
|
365
|
-
{ section: 'Human gate', decisionId: decision.id, missing },
|
|
366
|
-
),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function absentLegacyReviewSeats(reviewTrail) {
|
|
372
|
-
if (typeof reviewTrail !== 'string') return [];
|
|
373
|
-
const seats = [];
|
|
374
|
-
for (const line of reviewTrail.split('\n')) {
|
|
375
|
-
if (!line.trim().startsWith('|')) continue;
|
|
376
|
-
const cells = line
|
|
377
|
-
.split('|')
|
|
378
|
-
.slice(1, -1)
|
|
379
|
-
.map((cell) => cell.trim());
|
|
380
|
-
if (cells.length < 2 || !/^absent(?:\b|\s*[:—-])/iu.test(cells[1])) continue;
|
|
381
|
-
const seat = cells[0].replace(/[`*_]/gu, '').trim();
|
|
382
|
-
if (seat.length > 0 && !seats.includes(seat)) seats.push(seat);
|
|
383
|
-
}
|
|
384
|
-
return seats;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function legacyGateNamesAbsentSeat(gate, absentSeats) {
|
|
388
|
-
return absentSeats.some((seat) =>
|
|
389
|
-
new RegExp(
|
|
390
|
-
`\\bre[- ]run\\s+(?:\`${escapedPattern(seat)}\`|${escapedPattern(seat)})(?=$|[^\\p{L}\\p{N}-])`,
|
|
391
|
-
'iu',
|
|
392
|
-
).test(gate),
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function validateLegacyNoDecisionGate(gate, diagnostics, absentSeats) {
|
|
397
|
-
const contradictoryDispositions = ['adopt', 'reject', 'defer'].filter((disposition) =>
|
|
398
|
-
new RegExp(`\\b${disposition}\\b`, 'iu').test(gate),
|
|
399
|
-
);
|
|
400
|
-
if (contradictoryDispositions.length > 0) {
|
|
401
|
-
diagnostics.push(
|
|
402
|
-
diagnostic(
|
|
403
|
-
'E_OPERATE_REVIEW_HUMAN_GATE_CONTRADICTORY',
|
|
404
|
-
'A no-decision Human gate must not offer decision dispositions.',
|
|
405
|
-
{ section: 'Human gate', dispositions: contradictoryDispositions },
|
|
406
|
-
),
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
for (const [option, pattern] of [
|
|
410
|
-
['close', /\bclose\b/iu],
|
|
411
|
-
['supply evidence', /\bsupply\s+evidence\b/iu],
|
|
412
|
-
]) {
|
|
413
|
-
if (!pattern.test(gate)) {
|
|
414
|
-
diagnostics.push(
|
|
415
|
-
diagnostic(
|
|
416
|
-
'E_OPERATE_REVIEW_HUMAN_GATE_INCOMPLETE',
|
|
417
|
-
`A no-decision Human gate must offer ${option}.`,
|
|
418
|
-
{ section: 'Human gate', option },
|
|
419
|
-
),
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (absentSeats.length > 0 && !legacyGateNamesAbsentSeat(gate, absentSeats)) {
|
|
424
|
-
diagnostics.push(
|
|
425
|
-
diagnostic(
|
|
426
|
-
'E_OPERATE_REVIEW_HUMAN_GATE_INCOMPLETE',
|
|
427
|
-
'A no-decision Human gate with an absent review seat must offer re-run for a named absent seat.',
|
|
428
|
-
{ section: 'Human gate', option: 're-run <seat>', absentSeats },
|
|
429
|
-
),
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function validateLegacyDecisionLedger(
|
|
435
|
-
sectionBodies,
|
|
436
|
-
itemCount,
|
|
437
|
-
diagnostics,
|
|
438
|
-
{ requireExecutiveCallSection = false, requireNoDecisionOptions = false } = {},
|
|
439
|
-
) {
|
|
440
|
-
const executiveCall = sectionBodies.get('Executive call') ?? '';
|
|
441
|
-
if (requireExecutiveCallSection && executiveCall.length === 0) {
|
|
442
|
-
diagnostics.push(
|
|
443
|
-
diagnostic(
|
|
444
|
-
'E_OPERATE_REVIEW_EXECUTIVE_CALL_MISSING',
|
|
445
|
-
'Executive call must lead with one concise decision signal.',
|
|
446
|
-
{ section: 'Executive call' },
|
|
447
|
-
),
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const actionPlan = sectionBodies.get('Action plan') ?? '';
|
|
452
|
-
if (itemCount > 0) {
|
|
453
|
-
const header =
|
|
454
|
-
/^\|\s*ID\s*\|\s*Priority\s*\|\s*Action\s*\|\s*Owner\s*\|\s*First step\s*\|\s*Success measure\s*\|\s*Verification\s*\|\s*Depends on\s*\|\s*$/mu;
|
|
455
|
-
const action = /^\|\s*A[1-9][0-9]*\s*\|/mu;
|
|
456
|
-
if (!header.test(actionPlan) || !action.test(actionPlan)) {
|
|
457
|
-
diagnostics.push(
|
|
458
|
-
diagnostic(
|
|
459
|
-
'E_OPERATE_REVIEW_ACTION_PLAN_INVALID',
|
|
460
|
-
'A non-empty decision queue requires the contracted action table and at least one action row.',
|
|
461
|
-
{ section: 'Action plan' },
|
|
462
|
-
),
|
|
463
|
-
);
|
|
464
|
-
} else {
|
|
465
|
-
const actionRows = actionPlan
|
|
466
|
-
.split('\n')
|
|
467
|
-
.filter((line) => /^\|\s*A[1-9][0-9]*\s*\|/u.test(line))
|
|
468
|
-
.map((line) =>
|
|
469
|
-
line
|
|
470
|
-
.split('|')
|
|
471
|
-
.slice(1, -1)
|
|
472
|
-
.map((cell) => cell.trim()),
|
|
473
|
-
);
|
|
474
|
-
if (actionRows.length > 7) {
|
|
475
|
-
diagnostics.push(
|
|
476
|
-
diagnostic(
|
|
477
|
-
'E_OPERATE_REVIEW_ACTION_LIMIT',
|
|
478
|
-
`Action plan has ${actionRows.length} actions; at most 7 are allowed.`,
|
|
479
|
-
{ section: 'Action plan', count: actionRows.length, maxItems: 7 },
|
|
480
|
-
),
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
for (const row of actionRows) {
|
|
484
|
-
if (row.length !== 8 || row.some((cell) => cell.length === 0 || PLACEHOLDER.test(cell))) {
|
|
485
|
-
diagnostics.push(
|
|
486
|
-
diagnostic(
|
|
487
|
-
'E_OPERATE_REVIEW_ACTION_ROW_INVALID',
|
|
488
|
-
`Action ${row[0] ?? '(unknown)'} must populate all eight contracted columns.`,
|
|
489
|
-
{ section: 'Action plan', actionId: row[0] ?? null },
|
|
490
|
-
),
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const gate = sectionBodies.get('Human gate') ?? '';
|
|
498
|
-
if (itemCount === 0) {
|
|
499
|
-
if (requireNoDecisionOptions) {
|
|
500
|
-
validateLegacyNoDecisionGate(
|
|
501
|
-
gate,
|
|
502
|
-
diagnostics,
|
|
503
|
-
absentLegacyReviewSeats(sectionBodies.get('Review trail')),
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const decisions = legacyBoardDecisions(sectionBodies.get('Decision queue'));
|
|
509
|
-
for (const decision of decisions) validateLegacyDecisionGate(gate, decision, diagnostics);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function validateCurrentDecisionLedger(
|
|
513
|
-
sectionBodies,
|
|
514
|
-
itemCount,
|
|
515
|
-
diagnostics,
|
|
516
|
-
{ summarySection } = {},
|
|
517
|
-
) {
|
|
518
|
-
if (summarySection && (sectionBodies.get(summarySection) ?? '').trim().length === 0) {
|
|
519
|
-
diagnostics.push(
|
|
520
|
-
diagnostic(
|
|
521
|
-
'E_OPERATE_REVIEW_SUMMARY_MISSING',
|
|
522
|
-
`${summarySection} must contain a concise decision signal.`,
|
|
523
|
-
{ section: summarySection },
|
|
524
|
-
),
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const actionPlan = sectionBodies.get('Action plan') ?? '';
|
|
529
|
-
if (itemCount > 0) {
|
|
530
|
-
const header =
|
|
531
|
-
/^\|\s*ID\s*\|\s*Priority\s*\|\s*Action\s*\|\s*Suggested owner\s*\|\s*First step\s*\|\s*Success measure\s*\|\s*Check\s*\|\s*Depends on\s*\|\s*$/mu;
|
|
532
|
-
const action = /^\|\s*A[1-9][0-9]*\s*\|/mu;
|
|
533
|
-
if (!header.test(actionPlan) || !action.test(actionPlan)) {
|
|
534
|
-
diagnostics.push(
|
|
535
|
-
diagnostic(
|
|
536
|
-
'E_OPERATE_REVIEW_ACTION_PLAN_INVALID',
|
|
537
|
-
'A non-empty decision queue requires the contracted action table and at least one action row.',
|
|
538
|
-
{ section: 'Action plan' },
|
|
539
|
-
),
|
|
540
|
-
);
|
|
541
|
-
} else {
|
|
542
|
-
const actionRows = actionPlan
|
|
543
|
-
.split('\n')
|
|
544
|
-
.filter((line) => /^\|\s*A[1-9][0-9]*\s*\|/u.test(line))
|
|
545
|
-
.map((line) =>
|
|
546
|
-
line
|
|
547
|
-
.split('|')
|
|
548
|
-
.slice(1, -1)
|
|
549
|
-
.map((cell) => cell.trim()),
|
|
550
|
-
);
|
|
551
|
-
if (actionRows.length > 7) {
|
|
552
|
-
diagnostics.push(
|
|
553
|
-
diagnostic(
|
|
554
|
-
'E_OPERATE_REVIEW_ACTION_LIMIT',
|
|
555
|
-
`Action plan has ${actionRows.length} actions; at most 7 are allowed.`,
|
|
556
|
-
{ section: 'Action plan', count: actionRows.length, maxItems: 7 },
|
|
557
|
-
),
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
for (const row of actionRows) {
|
|
561
|
-
if (row.length !== 8 || row.some((cell) => cell.length === 0 || PLACEHOLDER.test(cell))) {
|
|
562
|
-
diagnostics.push(
|
|
563
|
-
diagnostic(
|
|
564
|
-
'E_OPERATE_REVIEW_ACTION_ROW_INVALID',
|
|
565
|
-
`Action ${row[0] ?? '(unknown)'} must populate all eight contracted columns.`,
|
|
566
|
-
{ section: 'Action plan', actionId: row[0] ?? null },
|
|
567
|
-
),
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/** Return structural quality diagnostics without changing or rejecting the note. */
|
|
576
|
-
export function inspectOperateReviewNote(
|
|
577
|
-
markdown,
|
|
578
|
-
{ profile, contractVersion = AUTO_CONTRACT_VERSION } = {},
|
|
579
|
-
) {
|
|
580
|
-
const source = canonicalText(markdown);
|
|
581
|
-
const detected = detectOperateReviewNoteContract(source, { profile, contractVersion });
|
|
582
|
-
const versionedContract = contractFor(detected.contractVersion);
|
|
583
|
-
const contract = profileFor(versionedContract, profile);
|
|
584
|
-
const diagnostics = [];
|
|
585
|
-
if (
|
|
586
|
-
detected.declaredContractVersion &&
|
|
587
|
-
detected.declaredContractVersion !== detected.contractVersion
|
|
588
|
-
) {
|
|
589
|
-
diagnostics.push(
|
|
590
|
-
diagnostic(
|
|
591
|
-
'E_OPERATE_REVIEW_CONTRACT_VERSION_MISMATCH',
|
|
592
|
-
`Note declares contract ${detected.declaredContractVersion} but ${detected.contractVersion} was requested.`,
|
|
593
|
-
{
|
|
594
|
-
declaredContractVersion: detected.declaredContractVersion,
|
|
595
|
-
requestedContractVersion: detected.contractVersion,
|
|
596
|
-
},
|
|
597
|
-
),
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
const titles = [...source.matchAll(/^#\s+\S.*$/gmu)];
|
|
601
|
-
if (titles.length === 0) {
|
|
602
|
-
diagnostics.push(
|
|
603
|
-
diagnostic(
|
|
604
|
-
'E_OPERATE_REVIEW_TITLE_MISSING',
|
|
605
|
-
'Review note requires one non-empty level-one title.',
|
|
606
|
-
),
|
|
607
|
-
);
|
|
608
|
-
} else if (titles.length > 1) {
|
|
609
|
-
diagnostics.push(
|
|
610
|
-
diagnostic(
|
|
611
|
-
'E_OPERATE_REVIEW_TITLE_DUPLICATE',
|
|
612
|
-
`Review note contains ${titles.length} level-one titles.`,
|
|
613
|
-
{ count: titles.length },
|
|
614
|
-
),
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
const byteLength = Buffer.byteLength(source, 'utf8');
|
|
618
|
-
if (byteLength > contract.maxBytes) {
|
|
619
|
-
diagnostics.push(
|
|
620
|
-
diagnostic(
|
|
621
|
-
'E_OPERATE_REVIEW_SIZE_LIMIT',
|
|
622
|
-
`Review note is ${byteLength} bytes; the ${profile} limit is ${contract.maxBytes}.`,
|
|
623
|
-
{ byteLength, maxBytes: contract.maxBytes },
|
|
624
|
-
),
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
const firstSection = source.search(/^##\s+/mu);
|
|
628
|
-
const preamble = source.slice(0, firstSection === -1 ? source.length : firstSection);
|
|
629
|
-
requireFields(preamble, contract.summaryFields, diagnostics, 'summary', contract.summaryValues);
|
|
630
|
-
const sectionBodies = validateSections(source, contract, diagnostics);
|
|
631
|
-
validateSectionItemLimits(sectionBodies, contract.sectionItemLimits, diagnostics);
|
|
632
|
-
for (const [section, fields] of Object.entries(contract.sectionFields)) {
|
|
633
|
-
if (!sectionBodies.has(section)) continue;
|
|
634
|
-
const body = sectionBodies.get(section);
|
|
635
|
-
if (body === contract.sectionEmptyStates?.[section]) continue;
|
|
636
|
-
requireFields(body, fields, diagnostics, section);
|
|
637
|
-
}
|
|
638
|
-
const itemCount = validateItems(
|
|
639
|
-
sectionBodies.get(contract.item.section),
|
|
640
|
-
contract.item,
|
|
641
|
-
diagnostics,
|
|
642
|
-
);
|
|
643
|
-
if (detected.contractVersion === '1.0.0') {
|
|
644
|
-
if (profile === 'chair') validateLegacyDecisionLedger(sectionBodies, itemCount, diagnostics);
|
|
645
|
-
if (profile === 'board-report') {
|
|
646
|
-
validateLegacyDecisionLedger(sectionBodies, itemCount, diagnostics, {
|
|
647
|
-
requireExecutiveCallSection: true,
|
|
648
|
-
requireNoDecisionOptions: true,
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
} else {
|
|
652
|
-
if (profile === 'chair') validateCurrentDecisionLedger(sectionBodies, itemCount, diagnostics);
|
|
653
|
-
if (profile === 'board-report') {
|
|
654
|
-
validateCurrentDecisionLedger(sectionBodies, itemCount, diagnostics, {
|
|
655
|
-
summarySection: 'Executive summary',
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
return Object.freeze({
|
|
660
|
-
ok: diagnostics.length === 0,
|
|
661
|
-
profile,
|
|
662
|
-
contractVersion: detected.contractVersion,
|
|
663
|
-
contractKind: detected.contractKind,
|
|
664
|
-
versionSource: detected.versionSource,
|
|
665
|
-
byteLength,
|
|
666
|
-
itemCount,
|
|
667
|
-
diagnostics: Object.freeze(diagnostics),
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/** Strict opt-in assertion for callers that explicitly require a conforming durable artifact. */
|
|
672
|
-
export function validateOperateReviewNote(markdown, options) {
|
|
673
|
-
const result = inspectOperateReviewNote(markdown, options);
|
|
674
|
-
if (!result.ok) {
|
|
675
|
-
throw new SkillRuntimeError(
|
|
676
|
-
'E_OPERATE_REVIEW_NOTE_INVALID',
|
|
677
|
-
`Operate ${result.profile} note failed ${result.diagnostics.length} contract check${result.diagnostics.length === 1 ? '' : 's'}.`,
|
|
678
|
-
{
|
|
679
|
-
profile: result.profile,
|
|
680
|
-
contractVersion: result.contractVersion,
|
|
681
|
-
diagnostics: result.diagnostics,
|
|
682
|
-
},
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
return result;
|
|
686
|
-
}
|