@open-agent-toolkit/cli 0.3.18 → 0.3.20
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/README.md +2 -1
- package/assets/agents/oat-phase-implementer.md +104 -3
- package/assets/agents/oat-reviewer.md +20 -5
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/reference/cli-reference.md +15 -7
- package/assets/docs/reference/oat-directory-structure.md +2 -0
- package/assets/docs/reference/project-artifacts.md +72 -1
- package/assets/docs/reference/project-state-machine.md +55 -6
- package/assets/docs/skills/brainstorm.md +3 -1
- package/assets/docs/skills/index.md +13 -9
- package/assets/docs/workflows/advanced/autonomy.md +12 -2
- package/assets/docs/workflows/advanced/dispatch-ceiling.md +2 -2
- package/assets/docs/workflows/advanced/workflow-gates.md +25 -7
- package/assets/docs/workflows/approvals-and-automation.md +25 -9
- package/assets/docs/workflows/choose-workflow.md +24 -8
- package/assets/docs/workflows/ideas/index.md +1 -1
- package/assets/docs/workflows/ideas/lifecycle.md +3 -2
- package/assets/docs/workflows/projects/closeout/closeout-skills.md +83 -5
- package/assets/docs/workflows/projects/closeout/pr-flow.md +9 -1
- package/assets/docs/workflows/projects/execution/execution-skills.md +27 -2
- package/assets/docs/workflows/projects/execution/implementation-execution.md +15 -0
- package/assets/docs/workflows/projects/execution/picking-up-projects.md +20 -1
- package/assets/docs/workflows/projects/lifecycle.md +172 -9
- package/assets/docs/workflows/projects/planning/hill-checkpoints.md +18 -1
- package/assets/docs/workflows/projects/planning/planning-skills.md +13 -10
- package/assets/docs/workflows/projects/planning/starting-projects.md +138 -20
- package/assets/docs/workflows/projects/reviews/index.md +36 -2
- package/assets/docs/workflows/projects/reviews/review-flavors.md +7 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-brainstorm/SKILL.md +58 -9
- package/assets/skills/oat-brainstorm/references/destinations.md +12 -4
- package/assets/skills/oat-docs/SKILL.md +13 -4
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/SKILL.md +20 -8
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +156 -122
- package/assets/skills/oat-project-brief/SKILL.md +592 -0
- package/assets/skills/oat-project-brief/references/brief-contract.md +743 -0
- package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +412 -0
- package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +93 -0
- package/assets/skills/oat-project-capture/SKILL.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +16 -2
- package/assets/skills/oat-project-discover/SKILL.md +4 -2
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -1
- package/assets/skills/oat-project-document/SKILL.md +17 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +156 -122
- package/assets/skills/oat-project-goal/SKILL.md +697 -0
- package/assets/skills/oat-project-goal/references/docs/autonomy-contract.md +412 -0
- package/assets/skills/oat-project-goal/references/docs/gate-approval-record.md +93 -0
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +107 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +156 -122
- package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +13 -0
- package/assets/skills/oat-project-implement/references/phase-execution.md +173 -1
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +124 -3
- package/assets/skills/oat-project-import-plan/SKILL.md +5 -1
- package/assets/skills/oat-project-lite/SKILL.md +4 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +156 -122
- package/assets/skills/oat-project-new/SKILL.md +5 -1
- package/assets/skills/oat-project-next/SKILL.md +71 -14
- package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +13 -0
- package/assets/skills/oat-project-plan/SKILL.md +24 -3
- package/assets/skills/oat-project-plan-writing/SKILL.md +8 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +65 -3
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +156 -122
- package/assets/skills/oat-project-pr-progress/SKILL.md +125 -4
- package/assets/skills/oat-project-progress/SKILL.md +63 -8
- package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +13 -0
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +65 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +156 -122
- package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +13 -0
- package/assets/skills/oat-project-reconcile/SKILL.md +94 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +397 -12
- package/assets/skills/oat-project-review-receive/SKILL.md +155 -1
- package/assets/skills/oat-project-revise/SKILL.md +51 -4
- package/assets/skills/oat-project-summary/SKILL.md +150 -2
- package/assets/skills/oat-wave-execute/SKILL.md +3 -2
- package/assets/templates/implementation-brief.md +165 -0
- package/assets/templates/plan-brief.md +140 -0
- package/assets/templates/plan.md +1 -1
- package/assets/templates/state.md +8 -2
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts +119 -3
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -1
- package/dist/commands/project/closeout-check/closeout-invariant.js +454 -2
- package/dist/commands/project/closeout-check/index.d.ts +21 -3
- package/dist/commands/project/closeout-check/index.d.ts.map +1 -1
- package/dist/commands/project/closeout-check/index.js +190 -4
- package/dist/commands/project/complete-state/index.d.ts +2 -1
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +7 -1
- package/dist/commands/project/delivery/check.d.ts +66 -0
- package/dist/commands/project/delivery/check.d.ts.map +1 -0
- package/dist/commands/project/delivery/check.js +232 -0
- package/dist/commands/project/delivery/evidence.d.ts +171 -0
- package/dist/commands/project/delivery/evidence.d.ts.map +1 -0
- package/dist/commands/project/delivery/evidence.js +902 -0
- package/dist/commands/project/delivery/index.d.ts +7 -0
- package/dist/commands/project/delivery/index.d.ts.map +1 -0
- package/dist/commands/project/delivery/index.js +13 -0
- package/dist/commands/project/delivery/refresh.d.ts +110 -0
- package/dist/commands/project/delivery/refresh.d.ts.map +1 -0
- package/dist/commands/project/delivery/refresh.js +441 -0
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/log/append.d.ts +6 -0
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +6 -1
- package/dist/commands/project/new/scaffold.d.ts +1 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +81 -7
- package/dist/commands/project/promote/promote.d.ts +46 -1
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +646 -5
- package/dist/commands/project/validate-plan/frontmatter.d.ts +12 -0
- package/dist/commands/project/validate-plan/frontmatter.d.ts.map +1 -0
- package/dist/commands/project/validate-plan/frontmatter.js +27 -0
- package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/index.js +62 -18
- package/dist/commands/project/validate-plan/profiles.d.ts +69 -0
- package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -0
- package/dist/commands/project/validate-plan/profiles.js +420 -0
- package/dist/commands/project/validate-plan/validate-plan.d.ts +3 -19
- package/dist/commands/project/validate-plan/validate-plan.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/validate-plan.js +5 -82
- package/dist/commands/shared/frontmatter.d.ts +1 -1
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +2 -0
- package/dist/commands/state/generate.d.ts.map +1 -1
- package/dist/commands/state/generate.js +82 -2
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type FrontmatterResult = {
|
|
2
|
+
kind: 'ok';
|
|
3
|
+
data: Record<string, unknown>;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'no-frontmatter';
|
|
6
|
+
data: Record<string, unknown>;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'invalid';
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseFrontmatterFromContent(content: string): FrontmatterResult;
|
|
12
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/frontmatter.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,GACd,iBAAiB,CAyBnB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/;
|
|
3
|
+
export function parseFrontmatterFromContent(content) {
|
|
4
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
5
|
+
if (!match?.[1]) {
|
|
6
|
+
return { kind: 'no-frontmatter', data: {} };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const parsed = YAML.parse(match[1]);
|
|
10
|
+
if (parsed !== null &&
|
|
11
|
+
typeof parsed === 'object' &&
|
|
12
|
+
!Array.isArray(parsed)) {
|
|
13
|
+
return { kind: 'ok', data: parsed };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
kind: 'invalid',
|
|
17
|
+
message: 'Frontmatter did not parse as an object',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
+
return {
|
|
23
|
+
kind: 'invalid',
|
|
24
|
+
message: `Malformed YAML in frontmatter: ${message}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,UAAU,+BAA+B;IACvC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;CACjE;AAMD,wBAAgB,gCAAgC,CAC9C,SAAS,GAAE,OAAO,CAAC,+BAA+B,CAAM,GACvD,OAAO,CAgJT"}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
4
4
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
|
+
import { WORKFLOW_MODES, } from '@open-agent-toolkit/control-plane';
|
|
5
6
|
import { Command } from 'commander';
|
|
6
|
-
import {
|
|
7
|
+
import { extractPhaseIds, parseFrontmatterFromContent, resolvePlanProfile, validateParallelGroups, validatePlanForProfile, } from './validate-plan.js';
|
|
8
|
+
function toWorkflowMode(value) {
|
|
9
|
+
return typeof value === 'string' &&
|
|
10
|
+
WORKFLOW_MODES.includes(value)
|
|
11
|
+
? value
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
7
14
|
const DEFAULT_DEPENDENCIES = {
|
|
8
15
|
buildCommandContext,
|
|
9
16
|
};
|
|
@@ -15,7 +22,8 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
15
22
|
.option('--project-path <path>', 'project directory containing plan.md', process.cwd())
|
|
16
23
|
.action((options, command) => {
|
|
17
24
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
18
|
-
const
|
|
25
|
+
const projectPath = resolve(context.cwd, options.projectPath);
|
|
26
|
+
const planPath = join(projectPath, 'plan.md');
|
|
19
27
|
let content;
|
|
20
28
|
try {
|
|
21
29
|
content = readFileSync(planPath, 'utf-8');
|
|
@@ -46,27 +54,59 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
46
54
|
return;
|
|
47
55
|
}
|
|
48
56
|
const groups = frontmatterResult.data['oat_plan_parallel_groups'];
|
|
49
|
-
|
|
50
|
-
let workflowMode = null;
|
|
57
|
+
let stateFrontmatter = null;
|
|
51
58
|
try {
|
|
52
|
-
const stateContent = readFileSync(join(
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
? null
|
|
56
|
-
: stateFrontmatter.data['oat_workflow_mode'];
|
|
57
|
-
workflowMode = typeof mode === 'string' ? mode : null;
|
|
59
|
+
const stateContent = readFileSync(join(projectPath, 'state.md'), 'utf-8');
|
|
60
|
+
const parsedState = parseFrontmatterFromContent(stateContent);
|
|
61
|
+
stateFrontmatter =
|
|
62
|
+
parsedState.kind === 'invalid' ? null : parsedState.data;
|
|
58
63
|
}
|
|
59
64
|
catch {
|
|
60
|
-
|
|
65
|
+
stateFrontmatter = null;
|
|
61
66
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
67
|
+
const workflowMode = toWorkflowMode(stateFrontmatter?.['oat_workflow_mode']);
|
|
68
|
+
const profile = resolvePlanProfile(workflowMode);
|
|
69
|
+
const phaseIds = extractPhaseIds(content, profile);
|
|
70
|
+
let implementationContent = null;
|
|
71
|
+
if (profile === 'goal') {
|
|
72
|
+
try {
|
|
73
|
+
implementationContent = readFileSync(join(projectPath, 'implementation.md'), 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
implementationContent = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const issues = validatePlanForProfile(content, {
|
|
80
|
+
profile,
|
|
81
|
+
workflowMode,
|
|
82
|
+
stateFrontmatter,
|
|
83
|
+
implementationContent,
|
|
84
|
+
});
|
|
85
|
+
const profileErrors = issues
|
|
86
|
+
.filter((issue) => issue.severity === 'error')
|
|
87
|
+
.map((issue) => issue.message);
|
|
88
|
+
const warnings = issues
|
|
89
|
+
.filter((issue) => issue.severity === 'warning')
|
|
90
|
+
.map((issue) => issue.message);
|
|
91
|
+
const warningPayload = warnings.length > 0 ? { warnings } : {};
|
|
92
|
+
if (!context.json) {
|
|
93
|
+
for (const warning of warnings) {
|
|
94
|
+
context.logger.warn(warning);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (profileErrors.length > 0) {
|
|
64
98
|
if (context.json) {
|
|
65
|
-
context.logger.json({
|
|
99
|
+
context.logger.json({
|
|
100
|
+
valid: false,
|
|
101
|
+
errors: profileErrors,
|
|
102
|
+
...warningPayload,
|
|
103
|
+
});
|
|
66
104
|
}
|
|
67
105
|
else {
|
|
68
106
|
context.logger.error('Plan validation failed:');
|
|
69
|
-
|
|
107
|
+
for (const err of profileErrors) {
|
|
108
|
+
context.logger.error(` - ${err}`);
|
|
109
|
+
}
|
|
70
110
|
}
|
|
71
111
|
process.exitCode = 1;
|
|
72
112
|
return;
|
|
@@ -74,7 +114,7 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
74
114
|
const result = validateParallelGroups(groups, phaseIds);
|
|
75
115
|
if (result.valid) {
|
|
76
116
|
if (context.json) {
|
|
77
|
-
context.logger.json({ valid: true });
|
|
117
|
+
context.logger.json({ valid: true, ...warningPayload });
|
|
78
118
|
}
|
|
79
119
|
else {
|
|
80
120
|
context.logger.success('Plan validation passed.');
|
|
@@ -83,7 +123,11 @@ export function createProjectValidatePlanCommand(overrides = {}) {
|
|
|
83
123
|
return;
|
|
84
124
|
}
|
|
85
125
|
if (context.json) {
|
|
86
|
-
context.logger.json({
|
|
126
|
+
context.logger.json({
|
|
127
|
+
valid: false,
|
|
128
|
+
errors: result.errors,
|
|
129
|
+
...warningPayload,
|
|
130
|
+
});
|
|
87
131
|
}
|
|
88
132
|
else {
|
|
89
133
|
context.logger.error('Plan validation failed:');
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type MilestoneMembershipErrorCode, type WorkflowMode } from '@open-agent-toolkit/control-plane';
|
|
2
|
+
/**
|
|
3
|
+
* Plan validation profiles. Each workflow mode validates `plan.md` against
|
|
4
|
+
* one profile:
|
|
5
|
+
*
|
|
6
|
+
* - `task`: spec-driven, quick and import plans. Phase ids come from task
|
|
7
|
+
* headings; no profile-specific body rules.
|
|
8
|
+
* - `lite`: the lite single-phase rules.
|
|
9
|
+
* - `brief`: taskless phase blocks (`**Intent:**`, `**Exit evidence:**`,
|
|
10
|
+
* optional `**Provisional:**`). Phase ids come from `## Phase` headings.
|
|
11
|
+
* - `goal`: every `brief` rule plus the goal milestone rules: membership
|
|
12
|
+
* read through the control-plane Milestones reader (its error codes map
|
|
13
|
+
* onto `goal-*` codes), frozen delivery scopes from `oat_deliveries`,
|
|
14
|
+
* parallel groups inside one milestone, `oat_plan_source: goal`, and
|
|
15
|
+
* canonical phase ids in persisted registry lists.
|
|
16
|
+
*/
|
|
17
|
+
export type PlanProfile = 'task' | 'lite' | 'brief' | 'goal';
|
|
18
|
+
export interface ValidationContext {
|
|
19
|
+
profile: PlanProfile;
|
|
20
|
+
workflowMode: WorkflowMode | null;
|
|
21
|
+
stateFrontmatter: Record<string, unknown> | null;
|
|
22
|
+
/**
|
|
23
|
+
* The journal (`implementation.md`), when readable. Goal membership reads it
|
|
24
|
+
* so a phase the journal marks extracted need not belong to a milestone.
|
|
25
|
+
*/
|
|
26
|
+
implementationContent?: string | null;
|
|
27
|
+
}
|
|
28
|
+
export type BriefValidationErrorCode = 'brief-no-phases' | 'brief-duplicate-phase' | 'brief-phase-missing-intent' | 'brief-phase-missing-exit-evidence' | 'brief-provisional-invalid' | 'brief-first-phase-provisional' | 'brief-malformed-phase-heading';
|
|
29
|
+
export type BriefValidationWarningCode = 'brief-task-headings-ignored';
|
|
30
|
+
export type GoalValidationErrorCode = 'goal-milestones-missing' | 'goal-milestones-malformed' | 'goal-duplicate-milestone' | 'goal-milestone-empty' | 'goal-milestone-unknown-phase' | 'goal-phase-in-multiple-milestones' | 'goal-phase-without-milestone' | 'goal-shipped-milestone-changed' | 'goal-delivered-scope-changed' | 'goal-group-crosses-milestone' | 'goal-plan-source' | 'goal-noncanonical-phase-id';
|
|
31
|
+
export type LiteValidationErrorCode = 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
|
|
32
|
+
export type ValidationIssue = {
|
|
33
|
+
severity: 'error';
|
|
34
|
+
code: BriefValidationErrorCode | GoalValidationErrorCode | LiteValidationErrorCode;
|
|
35
|
+
message: string;
|
|
36
|
+
} | {
|
|
37
|
+
severity: 'warning';
|
|
38
|
+
code: BriefValidationWarningCode;
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function resolvePlanProfile(mode: WorkflowMode | null): PlanProfile;
|
|
42
|
+
export type LitePlanValidationResult = {
|
|
43
|
+
ok: true;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
code: 'lite-multi-phase' | 'lite-parallel-groups' | 'lite-criterion-without-command';
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function validateLitePlan(planContent: string, workflowMode: string | null | undefined): LitePlanValidationResult;
|
|
50
|
+
/**
|
|
51
|
+
* The normalized id and name of a `## Phase` heading line, or `null` when the
|
|
52
|
+
* line is not a well-formed phase heading. Delegates to the control-plane
|
|
53
|
+
* phase-unit parser so heading spellings (`## Phase 1:`, `## Phase p01:`,
|
|
54
|
+
* `## Phase p-rev1:`, `## Revision Phase 1:`) normalize exactly as progress
|
|
55
|
+
* reads them.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parsePhaseHeadingLine(line: string): {
|
|
58
|
+
phaseId: string;
|
|
59
|
+
name: string;
|
|
60
|
+
} | null;
|
|
61
|
+
export declare function extractPhaseIds(content: string, profile: PlanProfile): string[];
|
|
62
|
+
export declare function validatePlanForProfile(content: string, ctx: ValidationContext): ValidationIssue[];
|
|
63
|
+
/**
|
|
64
|
+
* Each Milestones reader error code maps onto exactly one goal rule code, so
|
|
65
|
+
* the reader stays the one membership predicate and validation never
|
|
66
|
+
* re-parses the table.
|
|
67
|
+
*/
|
|
68
|
+
export declare const GOAL_MEMBERSHIP_RULE_CODES: Readonly<Record<MilestoneMembershipErrorCode, GoalValidationErrorCode>>;
|
|
69
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,4BAA4B,EACjC,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAI3C;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,WAAW,CAAC;IACrB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACjD;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,MAAM,wBAAwB,GAChC,iBAAiB,GACjB,uBAAuB,GACvB,4BAA4B,GAC5B,mCAAmC,GACnC,2BAA2B,GAC3B,+BAA+B,GAC/B,+BAA+B,CAAC;AAEpC,MAAM,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AAEvE,MAAM,MAAM,uBAAuB,GAC/B,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,sBAAsB,GACtB,8BAA8B,GAC9B,mCAAmC,GACnC,8BAA8B,GAC9B,gCAAgC,GAChC,8BAA8B,GAC9B,8BAA8B,GAC9B,kBAAkB,GAClB,4BAA4B,CAAC;AAEjC,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,sBAAsB,GACtB,gCAAgC,CAAC;AAErC,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,WAAW,CAIzE;AAMD,MAAM,MAAM,wBAAwB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EACA,kBAAkB,GAClB,sBAAsB,GACtB,gCAAgC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAeN,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACtC,wBAAwB,CA8C1B;AAeD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,GACX;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG1C;AAiBD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,GACnB,MAAM,EAAE,CAIV;AA0LD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,iBAAiB,GACrB,eAAe,EAAE,CAenB;AAMD;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAC/C,MAAM,CAAC,4BAA4B,EAAE,uBAAuB,CAAC,CAS9D,CAAC"}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { isBriefMode, normalizeMilestoneId, normalizePhaseId, parseProvisionalPhaseIds, parseTaskProgress, readMilestoneMembership, recordLineMask, } from '@open-agent-toolkit/control-plane';
|
|
2
|
+
import { parseFrontmatterFromContent } from './frontmatter.js';
|
|
3
|
+
export function resolvePlanProfile(mode) {
|
|
4
|
+
if (mode === 'lite')
|
|
5
|
+
return 'lite';
|
|
6
|
+
if (isBriefMode(mode))
|
|
7
|
+
return mode;
|
|
8
|
+
return 'task';
|
|
9
|
+
}
|
|
10
|
+
function validationCriteria(planContent) {
|
|
11
|
+
const heading = /^## Validation Criteria[ \t]*$/m.exec(planContent);
|
|
12
|
+
if (!heading)
|
|
13
|
+
return null;
|
|
14
|
+
const bodyStart = heading.index + heading[0].length;
|
|
15
|
+
const remaining = planContent.slice(bodyStart);
|
|
16
|
+
const nextHeading = /^##\s+/m.exec(remaining);
|
|
17
|
+
const body = remaining.slice(0, nextHeading?.index ?? remaining.length);
|
|
18
|
+
return body
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter((line) => /^\s*-\s+/.test(line))
|
|
21
|
+
.map((line) => line.replace(/^\s*-\s+(?:\[[ xX]\]\s*)?/, '').trim());
|
|
22
|
+
}
|
|
23
|
+
export function validateLitePlan(planContent, workflowMode) {
|
|
24
|
+
if (workflowMode !== 'lite') {
|
|
25
|
+
return { ok: true };
|
|
26
|
+
}
|
|
27
|
+
const phaseCount = planContent.match(/^## Phase\b.*$/gm)?.length ?? 0;
|
|
28
|
+
if (phaseCount !== 1) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
code: 'lite-multi-phase',
|
|
32
|
+
message: `Lite plans must contain exactly one phase; found ${phaseCount}.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const frontmatter = parseFrontmatterFromContent(planContent);
|
|
36
|
+
const groups = frontmatter.kind === 'invalid'
|
|
37
|
+
? undefined
|
|
38
|
+
: frontmatter.data['oat_plan_parallel_groups'];
|
|
39
|
+
if (groups !== undefined &&
|
|
40
|
+
groups !== null &&
|
|
41
|
+
!(Array.isArray(groups) && groups.length === 0)) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
code: 'lite-parallel-groups',
|
|
45
|
+
message: 'Lite plans must declare no parallel groups.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const criteria = validationCriteria(planContent);
|
|
49
|
+
const invalidCriterion = criteria?.find((criterion) => !criterion.startsWith('manual:') && !/`[^`\n]+`/.test(criterion));
|
|
50
|
+
if (!criteria || criteria.length === 0 || invalidCriterion !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: 'lite-criterion-without-command',
|
|
54
|
+
message: 'Every lite Validation Criteria bullet must name a command in backticks or start with manual: after the bullet marker.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// phase id extraction
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function extractTaskHeadingPhaseIds(planContent) {
|
|
63
|
+
const matches = planContent.match(/### Task (p\d+)-t\d+/g) ?? [];
|
|
64
|
+
const ids = matches.map((m) => {
|
|
65
|
+
const parts = m.split(' ');
|
|
66
|
+
return parts[2].split('-')[0];
|
|
67
|
+
});
|
|
68
|
+
return Array.from(new Set(ids)).sort();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The normalized id and name of a `## Phase` heading line, or `null` when the
|
|
72
|
+
* line is not a well-formed phase heading. Delegates to the control-plane
|
|
73
|
+
* phase-unit parser so heading spellings (`## Phase 1:`, `## Phase p01:`,
|
|
74
|
+
* `## Phase p-rev1:`, `## Revision Phase 1:`) normalize exactly as progress
|
|
75
|
+
* reads them.
|
|
76
|
+
*/
|
|
77
|
+
export function parsePhaseHeadingLine(line) {
|
|
78
|
+
const phase = parseTaskProgress(line, '', { phaseUnits: true }).phases[0];
|
|
79
|
+
return phase ? { phaseId: phase.phaseId, name: phase.name } : null;
|
|
80
|
+
}
|
|
81
|
+
function phaseIdOfHeading(line) {
|
|
82
|
+
return parsePhaseHeadingLine(line)?.phaseId ?? null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalized `## Phase` heading ids in document order, outside fenced and
|
|
86
|
+
* indented code, each listed once.
|
|
87
|
+
*/
|
|
88
|
+
function extractBriefPhaseIds(planContent) {
|
|
89
|
+
const ids = parseTaskProgress(planContent, '', {
|
|
90
|
+
phaseUnits: true,
|
|
91
|
+
}).phases.map((phase) => phase.phaseId);
|
|
92
|
+
return Array.from(new Set(ids));
|
|
93
|
+
}
|
|
94
|
+
export function extractPhaseIds(content, profile) {
|
|
95
|
+
return profile === 'brief' || profile === 'goal'
|
|
96
|
+
? extractBriefPhaseIds(content)
|
|
97
|
+
: extractTaskHeadingPhaseIds(content);
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// brief profile
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Plan record lines, through the control-plane reader that phase-unit
|
|
104
|
+
* progress and quick plan readiness use, so a fenced example phase block
|
|
105
|
+
* never validates or fails a real plan.
|
|
106
|
+
*/
|
|
107
|
+
function recordLines(content) {
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
const mask = recordLineMask(lines);
|
|
110
|
+
return lines.filter((_, index) => mask[index]);
|
|
111
|
+
}
|
|
112
|
+
const PHASE_HEADING_CANDIDATE = /^##[ \t]+(?:Revision[ \t]+)?Phase\b/;
|
|
113
|
+
const SECTION_HEADING = /^#{1,2}[ \t]/;
|
|
114
|
+
const ANY_HEADING = /^#/;
|
|
115
|
+
const TASK_HEADING = /^###[ \t]+Task\b/;
|
|
116
|
+
const FIELD_MARKER = /^\*\*[^*\n]+:\*\*/;
|
|
117
|
+
const INTENT_MARKER = /^\*\*Intent:\*\*(.*)$/;
|
|
118
|
+
const EXIT_EVIDENCE_MARKER = /^\*\*Exit evidence:\*\*(.*)$/;
|
|
119
|
+
const PROVISIONAL_LINE = /^\*\*Provisional:\*\*/;
|
|
120
|
+
// The control-plane parser's grammar: a space after the marker, then the value.
|
|
121
|
+
const PROVISIONAL_VALID = /^\*\*Provisional:\*\*\s+(?:yes|no)$/;
|
|
122
|
+
/**
|
|
123
|
+
* Whether a phase block carries `marker` with content: a non-empty value on
|
|
124
|
+
* the marker line, or at least one continuation line before the next blank
|
|
125
|
+
* line or field marker.
|
|
126
|
+
*/
|
|
127
|
+
function hasFilledField(lines, marker) {
|
|
128
|
+
for (const [index, line] of lines.entries()) {
|
|
129
|
+
const match = line.trim().match(marker);
|
|
130
|
+
if (!match)
|
|
131
|
+
continue;
|
|
132
|
+
if ((match[1] ?? '').trim().length > 0)
|
|
133
|
+
return true;
|
|
134
|
+
const next = (lines[index + 1] ?? '').trim();
|
|
135
|
+
if (next.length > 0 && !FIELD_MARKER.test(next))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function readBriefPhaseBlocks(content) {
|
|
141
|
+
const blocks = [];
|
|
142
|
+
const malformed = [];
|
|
143
|
+
// Marker ownership ends at the next line starting with `#` (the phase block
|
|
144
|
+
// grammar); task-heading attribution lasts until the next `#`/`##` heading.
|
|
145
|
+
let owner = null;
|
|
146
|
+
let section = null;
|
|
147
|
+
for (const line of recordLines(content)) {
|
|
148
|
+
if (PHASE_HEADING_CANDIDATE.test(line)) {
|
|
149
|
+
const phaseId = phaseIdOfHeading(line);
|
|
150
|
+
if (phaseId == null) {
|
|
151
|
+
malformed.push(line.trim());
|
|
152
|
+
owner = null;
|
|
153
|
+
section = null;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const block = { phaseId, lines: [], taskHeadings: 0 };
|
|
157
|
+
blocks.push(block);
|
|
158
|
+
owner = block;
|
|
159
|
+
section = block;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (SECTION_HEADING.test(line)) {
|
|
163
|
+
owner = null;
|
|
164
|
+
section = null;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (TASK_HEADING.test(line) && section != null) {
|
|
168
|
+
section.taskHeadings += 1;
|
|
169
|
+
}
|
|
170
|
+
if (ANY_HEADING.test(line)) {
|
|
171
|
+
owner = null;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
owner?.lines.push(line);
|
|
175
|
+
}
|
|
176
|
+
return { blocks, malformed };
|
|
177
|
+
}
|
|
178
|
+
function validateBriefPlan(content) {
|
|
179
|
+
const issues = [];
|
|
180
|
+
const { blocks, malformed } = readBriefPhaseBlocks(content);
|
|
181
|
+
for (const heading of malformed) {
|
|
182
|
+
issues.push({
|
|
183
|
+
severity: 'error',
|
|
184
|
+
code: 'brief-malformed-phase-heading',
|
|
185
|
+
message: `brief-malformed-phase-heading: "${heading}" is not a phase heading; use "## Phase pNN: <name>".`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (blocks.length === 0) {
|
|
189
|
+
issues.push({
|
|
190
|
+
severity: 'error',
|
|
191
|
+
code: 'brief-no-phases',
|
|
192
|
+
message: 'brief-no-phases: Brief plans must declare at least one "## Phase pNN: <name>" block.',
|
|
193
|
+
});
|
|
194
|
+
return issues;
|
|
195
|
+
}
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
const reported = new Set();
|
|
198
|
+
for (const { phaseId } of blocks) {
|
|
199
|
+
if (seen.has(phaseId) && !reported.has(phaseId)) {
|
|
200
|
+
reported.add(phaseId);
|
|
201
|
+
issues.push({
|
|
202
|
+
severity: 'error',
|
|
203
|
+
code: 'brief-duplicate-phase',
|
|
204
|
+
message: `brief-duplicate-phase: Phase ${phaseId} is declared more than once.`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
seen.add(phaseId);
|
|
208
|
+
}
|
|
209
|
+
for (const block of blocks) {
|
|
210
|
+
if (!hasFilledField(block.lines, INTENT_MARKER)) {
|
|
211
|
+
issues.push({
|
|
212
|
+
severity: 'error',
|
|
213
|
+
code: 'brief-phase-missing-intent',
|
|
214
|
+
message: `brief-phase-missing-intent: Phase ${block.phaseId} has no non-empty **Intent:**.`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (!hasFilledField(block.lines, EXIT_EVIDENCE_MARKER)) {
|
|
218
|
+
issues.push({
|
|
219
|
+
severity: 'error',
|
|
220
|
+
code: 'brief-phase-missing-exit-evidence',
|
|
221
|
+
message: `brief-phase-missing-exit-evidence: Phase ${block.phaseId} has no non-empty **Exit evidence:**.`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
for (const line of block.lines) {
|
|
225
|
+
const trimmed = line.trim();
|
|
226
|
+
if (PROVISIONAL_LINE.test(trimmed) && !PROVISIONAL_VALID.test(trimmed)) {
|
|
227
|
+
issues.push({
|
|
228
|
+
severity: 'error',
|
|
229
|
+
code: 'brief-provisional-invalid',
|
|
230
|
+
message: `brief-provisional-invalid: Phase ${block.phaseId} has "${trimmed}"; use "**Provisional:** yes" or "**Provisional:** no".`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const firstPhaseId = blocks[0].phaseId;
|
|
236
|
+
if (parseProvisionalPhaseIds(content).has(firstPhaseId)) {
|
|
237
|
+
issues.push({
|
|
238
|
+
severity: 'error',
|
|
239
|
+
code: 'brief-first-phase-provisional',
|
|
240
|
+
message: `brief-first-phase-provisional: The first phase (${firstPhaseId}) cannot be provisional.`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const withTasks = blocks.filter((block) => block.taskHeadings > 0);
|
|
244
|
+
if (withTasks.length > 0) {
|
|
245
|
+
issues.push({
|
|
246
|
+
severity: 'warning',
|
|
247
|
+
code: 'brief-task-headings-ignored',
|
|
248
|
+
message: `brief-task-headings-ignored: Task headings in plan.md under ${withTasks
|
|
249
|
+
.map((block) => block.phaseId)
|
|
250
|
+
.join(', ')} do not count toward brief progress; record execution tasks in implementation.md.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return issues;
|
|
254
|
+
}
|
|
255
|
+
export function validatePlanForProfile(content, ctx) {
|
|
256
|
+
switch (ctx.profile) {
|
|
257
|
+
case 'lite': {
|
|
258
|
+
const result = validateLitePlan(content, 'lite');
|
|
259
|
+
return result.ok
|
|
260
|
+
? []
|
|
261
|
+
: [{ severity: 'error', code: result.code, message: result.message }];
|
|
262
|
+
}
|
|
263
|
+
case 'brief':
|
|
264
|
+
return validateBriefPlan(content);
|
|
265
|
+
case 'goal':
|
|
266
|
+
return [...validateBriefPlan(content), ...validateGoalPlan(content, ctx)];
|
|
267
|
+
case 'task':
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// goal profile
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
/**
|
|
275
|
+
* Each Milestones reader error code maps onto exactly one goal rule code, so
|
|
276
|
+
* the reader stays the one membership predicate and validation never
|
|
277
|
+
* re-parses the table.
|
|
278
|
+
*/
|
|
279
|
+
export const GOAL_MEMBERSHIP_RULE_CODES = {
|
|
280
|
+
'milestones-missing': 'goal-milestones-missing',
|
|
281
|
+
'milestones-malformed': 'goal-milestones-malformed',
|
|
282
|
+
'milestone-duplicate': 'goal-duplicate-milestone',
|
|
283
|
+
'milestone-empty': 'goal-milestone-empty',
|
|
284
|
+
'milestone-unknown-phase': 'goal-milestone-unknown-phase',
|
|
285
|
+
'phase-in-multiple-milestones': 'goal-phase-in-multiple-milestones',
|
|
286
|
+
'phase-without-milestone': 'goal-phase-without-milestone',
|
|
287
|
+
};
|
|
288
|
+
function goalError(code, message) {
|
|
289
|
+
return { severity: 'error', code, message: `${code}: ${message}` };
|
|
290
|
+
}
|
|
291
|
+
function isRecord(value) {
|
|
292
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
293
|
+
}
|
|
294
|
+
function rawList(value) {
|
|
295
|
+
return Array.isArray(value) ? value : [];
|
|
296
|
+
}
|
|
297
|
+
function readRawDeliveries(state) {
|
|
298
|
+
return rawList(state?.['oat_deliveries'])
|
|
299
|
+
.filter(isRecord)
|
|
300
|
+
.map((entry, index) => ({
|
|
301
|
+
label: typeof entry['delivery_id'] === 'string'
|
|
302
|
+
? entry['delivery_id']
|
|
303
|
+
: `oat_deliveries[${index}]`,
|
|
304
|
+
milestone: normalizeMilestoneId(entry['milestone']),
|
|
305
|
+
scope: rawList(entry['scope'])
|
|
306
|
+
.map((token) => normalizePhaseId(String(token)))
|
|
307
|
+
.filter((token) => token !== null),
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Persisted phase-id lists in the goal registry. The state reader normalizes
|
|
312
|
+
* these and drops tokens that are not phase ids; validation reports every
|
|
313
|
+
* token that is not already canonical so the drift is repaired at its source.
|
|
314
|
+
*/
|
|
315
|
+
function persistedPhaseIdLists(state) {
|
|
316
|
+
if (state === null)
|
|
317
|
+
return [];
|
|
318
|
+
const lists = [];
|
|
319
|
+
const carrierLists = (prefix, carrier) => {
|
|
320
|
+
if (isRecord(carrier) && isRecord(carrier['subject'])) {
|
|
321
|
+
lists.push({
|
|
322
|
+
field: `${prefix}.subject.scope`,
|
|
323
|
+
values: rawList(carrier['subject']['scope']),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
const recovery = state['oat_goal_recovery'];
|
|
328
|
+
if (isRecord(recovery)) {
|
|
329
|
+
lists.push({
|
|
330
|
+
field: 'oat_goal_recovery.phase_ids',
|
|
331
|
+
values: rawList(recovery['phase_ids']),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
for (const [index, entry] of rawList(state['oat_deliveries']).entries()) {
|
|
335
|
+
if (!isRecord(entry))
|
|
336
|
+
continue;
|
|
337
|
+
const prefix = `oat_deliveries[${index}]`;
|
|
338
|
+
lists.push({ field: `${prefix}.scope`, values: rawList(entry['scope']) });
|
|
339
|
+
if (isRecord(entry['integrated_review'])) {
|
|
340
|
+
lists.push({
|
|
341
|
+
field: `${prefix}.integrated_review.scope`,
|
|
342
|
+
values: rawList(entry['integrated_review']['scope']),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
carrierLists(`${prefix}.exit_gate`, entry['exit_gate']);
|
|
346
|
+
}
|
|
347
|
+
const gates = state['oat_milestone_gates'];
|
|
348
|
+
if (isRecord(gates)) {
|
|
349
|
+
for (const [milestone, carriers] of Object.entries(gates)) {
|
|
350
|
+
if (!isRecord(carriers))
|
|
351
|
+
continue;
|
|
352
|
+
carrierLists(`oat_milestone_gates.${milestone}.plan`, carriers['plan']);
|
|
353
|
+
carrierLists(`oat_milestone_gates.${milestone}.exit`, carriers['exit']);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return lists;
|
|
357
|
+
}
|
|
358
|
+
function validateGoalPlan(content, ctx) {
|
|
359
|
+
const issues = [];
|
|
360
|
+
const frontmatter = parseFrontmatterFromContent(content);
|
|
361
|
+
const planData = frontmatter.kind === 'invalid' ? {} : frontmatter.data;
|
|
362
|
+
if (planData['oat_plan_source'] !== 'goal') {
|
|
363
|
+
issues.push(goalError('goal-plan-source', `Goal plans must declare "oat_plan_source: goal"; found ${JSON.stringify(planData['oat_plan_source'] ?? null)}.`));
|
|
364
|
+
}
|
|
365
|
+
const membership = readMilestoneMembership(content, ctx.implementationContent ?? '');
|
|
366
|
+
for (const error of membership.errors) {
|
|
367
|
+
issues.push(goalError(GOAL_MEMBERSHIP_RULE_CODES[error.code], error.message));
|
|
368
|
+
}
|
|
369
|
+
const owners = new Map();
|
|
370
|
+
for (const row of membership.milestones) {
|
|
371
|
+
for (const phaseId of row.phaseIds) {
|
|
372
|
+
if (!owners.has(phaseId))
|
|
373
|
+
owners.set(phaseId, row.milestoneId);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const declaredMilestones = new Set(membership.milestones.map((row) => row.milestoneId));
|
|
377
|
+
const declaredPhases = new Set(extractBriefPhaseIds(content));
|
|
378
|
+
for (const delivery of readRawDeliveries(ctx.stateFrontmatter)) {
|
|
379
|
+
if (!membership.present || delivery.milestone === null)
|
|
380
|
+
continue;
|
|
381
|
+
if (!declaredMilestones.has(delivery.milestone)) {
|
|
382
|
+
issues.push(goalError('goal-shipped-milestone-changed', `Delivery ${delivery.label} shipped milestone ${delivery.milestone}, which the Milestones table no longer declares.`));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
for (const phaseId of delivery.scope) {
|
|
386
|
+
if (!declaredPhases.has(phaseId)) {
|
|
387
|
+
issues.push(goalError('goal-delivered-scope-changed', `Delivery ${delivery.label} froze ${phaseId} in its scope, but the brief no longer declares it.`));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const owner = owners.get(phaseId);
|
|
391
|
+
if (owner !== undefined && owner !== delivery.milestone) {
|
|
392
|
+
issues.push(goalError('goal-delivered-scope-changed', `Delivery ${delivery.label} froze ${phaseId} under ${delivery.milestone}, but the Milestones table now assigns it to ${owner}.`));
|
|
393
|
+
}
|
|
394
|
+
else if (owner === undefined) {
|
|
395
|
+
issues.push(goalError('goal-delivered-scope-changed', `Delivery ${delivery.label} froze ${phaseId} under ${delivery.milestone}, but the Milestones table no longer lists it there.`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const [index, group] of rawList(planData['oat_plan_parallel_groups']).entries()) {
|
|
400
|
+
const groupOwners = new Set();
|
|
401
|
+
for (const member of rawList(group)) {
|
|
402
|
+
const phaseId = normalizePhaseId(String(member));
|
|
403
|
+
const owner = phaseId === null ? undefined : owners.get(phaseId);
|
|
404
|
+
if (owner !== undefined)
|
|
405
|
+
groupOwners.add(owner);
|
|
406
|
+
}
|
|
407
|
+
if (groupOwners.size > 1) {
|
|
408
|
+
issues.push(goalError('goal-group-crosses-milestone', `Parallel group ${index} spans milestones ${[...groupOwners].join(', ')}; a group must sit inside one milestone.`));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const { field, values } of persistedPhaseIdLists(ctx.stateFrontmatter)) {
|
|
412
|
+
for (const value of values) {
|
|
413
|
+
const token = typeof value === 'string' ? value : String(value);
|
|
414
|
+
if (normalizePhaseId(token) !== token) {
|
|
415
|
+
issues.push(goalError('goal-noncanonical-phase-id', `${field} holds ${JSON.stringify(value)}, which is not a canonical phase id like "p01" or "p-rev1".`));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return issues;
|
|
420
|
+
}
|