@navels/neal 0.1.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/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function isPlanRefinementState(state) {
|
|
2
|
+
return state.topLevelMode === 'plan' && !state.derivedPlanPath;
|
|
3
|
+
}
|
|
4
|
+
export function formatPlanRefinementRoundLine(args) {
|
|
5
|
+
return `[neal] plan refinement mode: round ${args.round}/${args.maxRounds}`;
|
|
6
|
+
}
|
|
7
|
+
export function determinePlanRefinementConvergence(state) {
|
|
8
|
+
if (state.status === 'done') {
|
|
9
|
+
return 'converged';
|
|
10
|
+
}
|
|
11
|
+
if (state.status !== 'blocked') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (state.rounds.length === 0) {
|
|
15
|
+
return 'coder_blocked';
|
|
16
|
+
}
|
|
17
|
+
if (state.rounds.length >= state.maxRounds) {
|
|
18
|
+
return 'max_rounds';
|
|
19
|
+
}
|
|
20
|
+
return 'stuck';
|
|
21
|
+
}
|
|
22
|
+
export function formatPlanRefinementSummary(args) {
|
|
23
|
+
const parts = [`plan refined: ${args.rounds} rounds`];
|
|
24
|
+
if (args.backupPath) {
|
|
25
|
+
parts.push(`backup at ${args.backupPath}`);
|
|
26
|
+
}
|
|
27
|
+
parts.push(`convergence reason: ${args.convergenceReason}`);
|
|
28
|
+
const line = `[neal] ${parts.join(', ')}`;
|
|
29
|
+
if (args.residualNonBlocking > 0) {
|
|
30
|
+
return `${line} (${args.residualNonBlocking} residual non-blocking findings)`;
|
|
31
|
+
}
|
|
32
|
+
return line;
|
|
33
|
+
}
|
|
34
|
+
export function planRefinementExitCode(reason) {
|
|
35
|
+
return reason === 'converged' || reason === null ? 0 : 2;
|
|
36
|
+
}
|
|
37
|
+
export function countOpenNonBlockingFindings(state) {
|
|
38
|
+
return state.findings.filter((finding) => finding.status === 'open' && finding.severity === 'non_blocking').length;
|
|
39
|
+
}
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
const EXECUTION_SHAPE_HEADER = '## Execution Shape';
|
|
2
|
+
const EXECUTION_QUEUE_HEADER = '## Execution Queue';
|
|
3
|
+
const EXECUTION_LOOP_HEADER = '## Execution Loop';
|
|
4
|
+
const COMPLETION_CONDITION_HEADER = '## Completion Condition';
|
|
5
|
+
const SCOPE_HEADING_PATTERN = /^### Scope (\d+):(.*)$/;
|
|
6
|
+
const RECURRING_SCOPE_HEADING = '### Recurring Scope';
|
|
7
|
+
const REQUIRED_SCOPE_BULLETS = ['goal', 'verification', 'success condition'];
|
|
8
|
+
const EXECUTION_QUEUE_HEADER_ALIASES = new Set(['## Ordered Derived Scopes', '## Derived Execution Queue']);
|
|
9
|
+
const VERIFICATION_LABEL_ALIASES = new Set(['verification', 'verification strategy']);
|
|
10
|
+
const SUCCESS_CONDITION_LABEL_ALIASES = new Set(['success condition', 'exit criteria']);
|
|
11
|
+
export function normalizeExecutionShapeDeclaration(planDocument, executionShape) {
|
|
12
|
+
const lines = planDocument.split(/\r?\n/);
|
|
13
|
+
const section = findSection(lines, EXECUTION_SHAPE_HEADER);
|
|
14
|
+
const sectionLines = [EXECUTION_SHAPE_HEADER, '', `executionShape: ${executionShape}`];
|
|
15
|
+
if (section !== null) {
|
|
16
|
+
const before = trimTrailingBlankLines(lines.slice(0, section.start));
|
|
17
|
+
return [
|
|
18
|
+
...before,
|
|
19
|
+
...(before.length > 0 ? [''] : []),
|
|
20
|
+
...sectionLines,
|
|
21
|
+
'',
|
|
22
|
+
...trimLeadingBlankLines(lines.slice(section.end)),
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
|
25
|
+
const firstSecondLevelSection = lines.findIndex((line) => /^## /.test(line.trim()));
|
|
26
|
+
const insertIndex = firstSecondLevelSection === -1 ? lines.length : firstSecondLevelSection;
|
|
27
|
+
const before = trimTrailingBlankLines(lines.slice(0, insertIndex));
|
|
28
|
+
return [
|
|
29
|
+
...before,
|
|
30
|
+
...(before.length > 0 ? [''] : []),
|
|
31
|
+
...sectionLines,
|
|
32
|
+
'',
|
|
33
|
+
...trimLeadingBlankLines(lines.slice(insertIndex)),
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
export function validatePlanDocument(planDocument) {
|
|
37
|
+
const normalization = normalizePlanDocument(planDocument);
|
|
38
|
+
const lines = normalization.normalizedDocument.split(/\r?\n/);
|
|
39
|
+
const errors = [];
|
|
40
|
+
const executionShapeSection = findSection(lines, EXECUTION_SHAPE_HEADER);
|
|
41
|
+
const executionShape = validateExecutionShapeSection(lines, executionShapeSection, errors);
|
|
42
|
+
if (executionShape === 'multi_scope') {
|
|
43
|
+
validateExecutionQueueSection(lines, errors);
|
|
44
|
+
validateSectionAbsence(lines, EXECUTION_LOOP_HEADER, '`executionShape: multi_scope` must not include a `## Execution Loop` section.', errors);
|
|
45
|
+
validateSectionAbsence(lines, COMPLETION_CONDITION_HEADER, '`executionShape: multi_scope` must not include a `## Completion Condition` section.', errors);
|
|
46
|
+
}
|
|
47
|
+
if (executionShape === 'multi_scope_unknown') {
|
|
48
|
+
validateExecutionLoopSection(lines, errors);
|
|
49
|
+
validateCompletionConditionSection(lines, errors);
|
|
50
|
+
validateSectionAbsence(lines, EXECUTION_QUEUE_HEADER, '`executionShape: multi_scope_unknown` must not include a `## Execution Queue` section.', errors);
|
|
51
|
+
}
|
|
52
|
+
if (executionShape === 'one_shot') {
|
|
53
|
+
validateOneShotQueueAbsence(lines, errors);
|
|
54
|
+
validateSectionAbsence(lines, EXECUTION_LOOP_HEADER, '`executionShape: one_shot` must not include a `## Execution Loop` section.', errors);
|
|
55
|
+
validateSectionAbsence(lines, COMPLETION_CONDITION_HEADER, '`executionShape: one_shot` must not include a `## Completion Condition` section.', errors);
|
|
56
|
+
}
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
executionShape,
|
|
61
|
+
errors,
|
|
62
|
+
normalization,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (executionShape === null) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
executionShape: null,
|
|
69
|
+
errors: ['Internal validation error: execution shape resolved to null after successful validation.'],
|
|
70
|
+
normalization,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
executionShape,
|
|
76
|
+
errors: [],
|
|
77
|
+
normalization,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function trimLeadingBlankLines(lines) {
|
|
81
|
+
const trimmed = [...lines];
|
|
82
|
+
while (trimmed.length > 0 && trimmed[0]?.trim() === '') {
|
|
83
|
+
trimmed.shift();
|
|
84
|
+
}
|
|
85
|
+
return trimmed;
|
|
86
|
+
}
|
|
87
|
+
function trimTrailingBlankLines(lines) {
|
|
88
|
+
const trimmed = [...lines];
|
|
89
|
+
while (trimmed.length > 0 && trimmed.at(-1)?.trim() === '') {
|
|
90
|
+
trimmed.pop();
|
|
91
|
+
}
|
|
92
|
+
return trimmed;
|
|
93
|
+
}
|
|
94
|
+
function normalizePlanDocument(planDocument) {
|
|
95
|
+
const lines = planDocument.split(/\r?\n/);
|
|
96
|
+
const operations = [];
|
|
97
|
+
const scopeLabelMappings = [];
|
|
98
|
+
const normalizedLines = [...lines];
|
|
99
|
+
const queueSection = findSectionByHeaders(lines, [EXECUTION_QUEUE_HEADER, ...EXECUTION_QUEUE_HEADER_ALIASES]);
|
|
100
|
+
if (queueSection !== null) {
|
|
101
|
+
const queueHeader = lines[queueSection.start]?.trim();
|
|
102
|
+
if (queueHeader !== EXECUTION_QUEUE_HEADER && queueHeader !== undefined) {
|
|
103
|
+
normalizedLines[queueSection.start] = EXECUTION_QUEUE_HEADER;
|
|
104
|
+
operations.push(`Normalized execution queue header \`${queueHeader}\` to \`${EXECUTION_QUEUE_HEADER}\`.`);
|
|
105
|
+
}
|
|
106
|
+
const normalizedQueueLines = normalizeExecutionQueueLines(getSectionContentLines(lines, queueSection), operations, scopeLabelMappings);
|
|
107
|
+
if (normalizedQueueLines !== null) {
|
|
108
|
+
normalizedLines.splice(queueSection.start + 1, queueSection.end - queueSection.start - 1, ...normalizedQueueLines);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const loopSection = findSection(lines, EXECUTION_LOOP_HEADER);
|
|
112
|
+
if (loopSection !== null) {
|
|
113
|
+
const normalizedLoopLines = normalizeExecutionLoopLines(getSectionContentLines(lines, loopSection));
|
|
114
|
+
if (normalizedLoopLines !== null) {
|
|
115
|
+
normalizedLines.splice(loopSection.start + 1, loopSection.end - loopSection.start - 1, ...normalizedLoopLines);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const normalizedDocument = normalizedLines.join('\n');
|
|
119
|
+
return {
|
|
120
|
+
applied: normalizedDocument !== planDocument,
|
|
121
|
+
normalizedDocument,
|
|
122
|
+
operations,
|
|
123
|
+
scopeLabelMappings,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function normalizeExecutionQueueLines(lines, operations, scopeLabelMappings) {
|
|
127
|
+
const queueDraft = collectNormalizedScopes(lines);
|
|
128
|
+
if (queueDraft === null) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const { scopes, sawAliasScopeHeading } = queueDraft;
|
|
132
|
+
let applied = false;
|
|
133
|
+
const normalizedLines = [];
|
|
134
|
+
scopes.forEach((scope, index) => {
|
|
135
|
+
const normalizedScopeNumber = index + 1;
|
|
136
|
+
const canonicalHeading = sawAliasScopeHeading
|
|
137
|
+
? `### Scope ${normalizedScopeNumber}: ${scope.title}`
|
|
138
|
+
: scope.heading.trim();
|
|
139
|
+
const originalHeading = scope.heading.trim();
|
|
140
|
+
if (originalHeading !== canonicalHeading) {
|
|
141
|
+
applied = true;
|
|
142
|
+
operations.push(`Normalized scope heading \`${originalHeading}\` to \`${canonicalHeading}\`.`);
|
|
143
|
+
}
|
|
144
|
+
if (sawAliasScopeHeading) {
|
|
145
|
+
scopeLabelMappings.push({
|
|
146
|
+
normalizedScopeNumber,
|
|
147
|
+
originalScopeLabel: scope.originalLabel,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
normalizedLines.push(canonicalHeading);
|
|
151
|
+
for (const line of scope.body) {
|
|
152
|
+
const normalizedLine = normalizeScopeBodyLine(line, scope.originalLabel, normalizedScopeNumber, sawAliasScopeHeading);
|
|
153
|
+
if (normalizedLine !== line) {
|
|
154
|
+
applied = true;
|
|
155
|
+
}
|
|
156
|
+
normalizedLines.push(normalizedLine);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (!applied) {
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
return normalizedLines;
|
|
163
|
+
}
|
|
164
|
+
function normalizeExecutionLoopLines(lines) {
|
|
165
|
+
const recurringScope = collectNormalizedRecurringScope(lines);
|
|
166
|
+
if (recurringScope === null) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
let applied = false;
|
|
170
|
+
const normalizedLines = [recurringScope.heading];
|
|
171
|
+
for (const line of recurringScope.body) {
|
|
172
|
+
const normalizedLine = normalizeScopeBodyLine(line, 'Recurring Scope', 1, false);
|
|
173
|
+
if (normalizedLine !== line) {
|
|
174
|
+
applied = true;
|
|
175
|
+
}
|
|
176
|
+
normalizedLines.push(normalizedLine);
|
|
177
|
+
}
|
|
178
|
+
return applied ? normalizedLines : lines;
|
|
179
|
+
}
|
|
180
|
+
function collectNormalizedScopes(lines) {
|
|
181
|
+
const scopes = [];
|
|
182
|
+
let currentScope = null;
|
|
183
|
+
let sawScope = false;
|
|
184
|
+
let sawAliasScopeHeading = false;
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
const trimmed = line.trim();
|
|
187
|
+
if (trimmed === '') {
|
|
188
|
+
if (currentScope !== null) {
|
|
189
|
+
currentScope.body.push(line);
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const canonicalHeadingMatch = trimmed.match(SCOPE_HEADING_PATTERN);
|
|
194
|
+
if (canonicalHeadingMatch !== null) {
|
|
195
|
+
sawScope = true;
|
|
196
|
+
if (currentScope !== null) {
|
|
197
|
+
scopes.push(currentScope);
|
|
198
|
+
}
|
|
199
|
+
currentScope = {
|
|
200
|
+
heading: trimmed,
|
|
201
|
+
originalLabel: canonicalHeadingMatch[1] ?? '',
|
|
202
|
+
title: canonicalHeadingMatch[2]?.trim() ?? '',
|
|
203
|
+
body: [],
|
|
204
|
+
};
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const aliasHeading = parseAliasScopeHeading(trimmed);
|
|
208
|
+
if (aliasHeading !== null) {
|
|
209
|
+
sawScope = true;
|
|
210
|
+
sawAliasScopeHeading = true;
|
|
211
|
+
if (currentScope !== null) {
|
|
212
|
+
scopes.push(currentScope);
|
|
213
|
+
}
|
|
214
|
+
currentScope = {
|
|
215
|
+
heading: trimmed,
|
|
216
|
+
originalLabel: aliasHeading.originalLabel,
|
|
217
|
+
title: aliasHeading.title,
|
|
218
|
+
body: [],
|
|
219
|
+
};
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (currentScope !== null) {
|
|
223
|
+
currentScope.body.push(line);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (currentScope !== null) {
|
|
229
|
+
scopes.push(currentScope);
|
|
230
|
+
}
|
|
231
|
+
return sawScope ? { scopes, sawAliasScopeHeading } : null;
|
|
232
|
+
}
|
|
233
|
+
function collectNormalizedRecurringScope(lines) {
|
|
234
|
+
let currentScope = null;
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
const trimmed = line.trim();
|
|
237
|
+
if (trimmed === '') {
|
|
238
|
+
if (currentScope !== null) {
|
|
239
|
+
currentScope.body.push(line);
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (trimmed === RECURRING_SCOPE_HEADING) {
|
|
244
|
+
if (currentScope !== null) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
currentScope = {
|
|
248
|
+
heading: trimmed,
|
|
249
|
+
body: [],
|
|
250
|
+
};
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (/^### /.test(trimmed)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (currentScope !== null) {
|
|
257
|
+
currentScope.body.push(line);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return currentScope;
|
|
263
|
+
}
|
|
264
|
+
function parseAliasScopeHeading(trimmed) {
|
|
265
|
+
const headingAliasMatch = trimmed.match(/^### Scope ([^:]+):(.*)$/);
|
|
266
|
+
if (headingAliasMatch !== null) {
|
|
267
|
+
const originalLabel = headingAliasMatch[1]?.trim() ?? '';
|
|
268
|
+
const title = headingAliasMatch[2]?.trim() ?? '';
|
|
269
|
+
if (originalLabel !== '' && !/^\d+$/.test(originalLabel) && title !== '') {
|
|
270
|
+
return { originalLabel, title };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const numberedAliasMatch = trimmed.match(/^\d+\.\s+Scope\s+([^:]+):\s*(.+)$/);
|
|
274
|
+
if (numberedAliasMatch !== null) {
|
|
275
|
+
const originalLabel = numberedAliasMatch[1]?.trim() ?? '';
|
|
276
|
+
const title = numberedAliasMatch[2]?.trim() ?? '';
|
|
277
|
+
if (originalLabel !== '' && title !== '') {
|
|
278
|
+
return { originalLabel, title };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function normalizeScopeBodyLine(line, originalLabel, normalizedScopeNumber, preserveOriginalScopeLabel) {
|
|
284
|
+
const bulletMatch = line.match(/^(\s*-\s+)([^:]+):(.*)$/);
|
|
285
|
+
if (bulletMatch === null) {
|
|
286
|
+
return line;
|
|
287
|
+
}
|
|
288
|
+
const prefix = bulletMatch[1] ?? '- ';
|
|
289
|
+
const label = bulletMatch[2]?.trim() ?? '';
|
|
290
|
+
const rest = bulletMatch[3] ?? '';
|
|
291
|
+
const normalizedLabel = normalizeBulletLabel(label);
|
|
292
|
+
let normalizedRest = rest;
|
|
293
|
+
if (preserveOriginalScopeLabel &&
|
|
294
|
+
normalizedLabel === 'Goal' &&
|
|
295
|
+
originalLabel !== String(normalizedScopeNumber) &&
|
|
296
|
+
!rest.includes(`Former derived scope ${originalLabel}`)) {
|
|
297
|
+
normalizedRest = ` (Former derived scope ${originalLabel})${rest}`;
|
|
298
|
+
}
|
|
299
|
+
return `${prefix}${normalizedLabel}:${normalizedRest}`;
|
|
300
|
+
}
|
|
301
|
+
function normalizeBulletLabel(label) {
|
|
302
|
+
const normalized = label.trim().toLowerCase();
|
|
303
|
+
if (normalized === 'goal') {
|
|
304
|
+
return 'Goal';
|
|
305
|
+
}
|
|
306
|
+
if (VERIFICATION_LABEL_ALIASES.has(normalized)) {
|
|
307
|
+
return 'Verification';
|
|
308
|
+
}
|
|
309
|
+
if (SUCCESS_CONDITION_LABEL_ALIASES.has(normalized)) {
|
|
310
|
+
return 'Success Condition';
|
|
311
|
+
}
|
|
312
|
+
return label.trim();
|
|
313
|
+
}
|
|
314
|
+
function validateExecutionShapeSection(lines, section, errors) {
|
|
315
|
+
if (section === null) {
|
|
316
|
+
errors.push('Missing required `## Execution Shape` section.');
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const contentLines = getSectionContentLines(lines, section).filter((line) => line.trim() !== '');
|
|
320
|
+
if (contentLines.length === 0) {
|
|
321
|
+
errors.push('`## Execution Shape` must contain exactly one non-empty line.');
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (contentLines.length > 1) {
|
|
325
|
+
errors.push('`## Execution Shape` must contain exactly one non-empty line.');
|
|
326
|
+
}
|
|
327
|
+
const declaredShape = contentLines[0]?.trim();
|
|
328
|
+
if (declaredShape === 'executionShape: one_shot') {
|
|
329
|
+
return 'one_shot';
|
|
330
|
+
}
|
|
331
|
+
if (declaredShape === 'executionShape: multi_scope') {
|
|
332
|
+
return 'multi_scope';
|
|
333
|
+
}
|
|
334
|
+
if (declaredShape === 'executionShape: multi_scope_unknown') {
|
|
335
|
+
return 'multi_scope_unknown';
|
|
336
|
+
}
|
|
337
|
+
errors.push('`## Execution Shape` must declare exactly `executionShape: one_shot`, `executionShape: multi_scope`, or `executionShape: multi_scope_unknown`.');
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
function validateExecutionQueueSection(lines, errors) {
|
|
341
|
+
const queueSection = findSection(lines, EXECUTION_QUEUE_HEADER);
|
|
342
|
+
if (queueSection === null) {
|
|
343
|
+
errors.push('`executionShape: multi_scope` requires a `## Execution Queue` section.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const contentLines = getSectionContentLines(lines, queueSection);
|
|
347
|
+
const scopes = collectScopes(contentLines, errors);
|
|
348
|
+
if (scopes.length === 0) {
|
|
349
|
+
errors.push('`## Execution Queue` must contain at least one `### Scope N:` entry.');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
validateScopeOrdering(scopes, errors);
|
|
353
|
+
for (const scope of scopes) {
|
|
354
|
+
validateScopeBullets(scope, errors);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function validateOneShotQueueAbsence(lines, errors) {
|
|
358
|
+
const queueSection = findSection(lines, EXECUTION_QUEUE_HEADER);
|
|
359
|
+
if (queueSection !== null) {
|
|
360
|
+
errors.push('`executionShape: one_shot` must not include a `## Execution Queue` section.');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function validateExecutionLoopSection(lines, errors) {
|
|
364
|
+
const loopSection = findSection(lines, EXECUTION_LOOP_HEADER);
|
|
365
|
+
if (loopSection === null) {
|
|
366
|
+
errors.push('`executionShape: multi_scope_unknown` requires a `## Execution Loop` section.');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const contentLines = getSectionContentLines(lines, loopSection);
|
|
370
|
+
const recurringScope = collectRecurringScope(contentLines, errors);
|
|
371
|
+
if (recurringScope === null) {
|
|
372
|
+
errors.push('`## Execution Loop` must contain exactly one literal `### Recurring Scope` entry.');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
validateRecurringScopeBullets(recurringScope, errors);
|
|
376
|
+
}
|
|
377
|
+
function validateCompletionConditionSection(lines, errors) {
|
|
378
|
+
const section = findSection(lines, COMPLETION_CONDITION_HEADER);
|
|
379
|
+
if (section === null) {
|
|
380
|
+
errors.push('`executionShape: multi_scope_unknown` requires a `## Completion Condition` section.');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const contentLines = getSectionContentLines(lines, section).filter((line) => line.trim() !== '');
|
|
384
|
+
if (contentLines.length === 0) {
|
|
385
|
+
errors.push('`## Completion Condition` must contain at least one non-empty line.');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function validateSectionAbsence(lines, header, message, errors) {
|
|
389
|
+
if (findSection(lines, header) !== null) {
|
|
390
|
+
errors.push(message);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function findSection(lines, header) {
|
|
394
|
+
return findSectionByHeaders(lines, [header]);
|
|
395
|
+
}
|
|
396
|
+
function findSectionByHeaders(lines, headers) {
|
|
397
|
+
const start = lines.findIndex((line) => headers.includes(line.trim()));
|
|
398
|
+
if (start === -1) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
402
|
+
if (/^## /.test(lines[index].trim())) {
|
|
403
|
+
return { start, end: index };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { start, end: lines.length };
|
|
407
|
+
}
|
|
408
|
+
function getSectionContentLines(lines, section) {
|
|
409
|
+
return lines.slice(section.start + 1, section.end);
|
|
410
|
+
}
|
|
411
|
+
function collectScopes(lines, errors) {
|
|
412
|
+
const scopes = [];
|
|
413
|
+
let currentScope = null;
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
const trimmed = line.trim();
|
|
416
|
+
if (trimmed === '') {
|
|
417
|
+
if (currentScope !== null) {
|
|
418
|
+
currentScope.body.push(line);
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const headingMatch = trimmed.match(SCOPE_HEADING_PATTERN);
|
|
423
|
+
if (headingMatch !== null) {
|
|
424
|
+
if (currentScope !== null) {
|
|
425
|
+
scopes.push(currentScope);
|
|
426
|
+
}
|
|
427
|
+
currentScope = {
|
|
428
|
+
number: Number.parseInt(headingMatch[1] ?? '', 10),
|
|
429
|
+
heading: trimmed,
|
|
430
|
+
body: [],
|
|
431
|
+
};
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (/^### /.test(trimmed)) {
|
|
435
|
+
errors.push(`\`## Execution Queue\` contains invalid scope heading \`${trimmed}\`; expected literal \`### Scope N:\` entries.`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (currentScope !== null) {
|
|
439
|
+
currentScope.body.push(line);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
errors.push(`\`## Execution Queue\` contains content before the first scope entry: \`${trimmed}\`.`);
|
|
443
|
+
}
|
|
444
|
+
if (currentScope !== null) {
|
|
445
|
+
scopes.push(currentScope);
|
|
446
|
+
}
|
|
447
|
+
return scopes;
|
|
448
|
+
}
|
|
449
|
+
function collectRecurringScope(lines, errors) {
|
|
450
|
+
let recurringScope = null;
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
const trimmed = line.trim();
|
|
453
|
+
if (trimmed === '') {
|
|
454
|
+
if (recurringScope !== null) {
|
|
455
|
+
recurringScope.body.push(line);
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (trimmed === RECURRING_SCOPE_HEADING) {
|
|
460
|
+
if (recurringScope !== null) {
|
|
461
|
+
errors.push('`## Execution Loop` must contain exactly one literal `### Recurring Scope` entry.');
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
recurringScope = {
|
|
465
|
+
heading: trimmed,
|
|
466
|
+
body: [],
|
|
467
|
+
};
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (/^### /.test(trimmed)) {
|
|
471
|
+
errors.push(`\`## Execution Loop\` contains invalid scope heading \`${trimmed}\`; expected literal \`${RECURRING_SCOPE_HEADING}\`.`);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (recurringScope !== null) {
|
|
475
|
+
recurringScope.body.push(line);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
errors.push(`\`## Execution Loop\` contains content before the recurring scope entry: \`${trimmed}\`.`);
|
|
479
|
+
}
|
|
480
|
+
return recurringScope;
|
|
481
|
+
}
|
|
482
|
+
function validateScopeOrdering(scopes, errors) {
|
|
483
|
+
scopes.forEach((scope, index) => {
|
|
484
|
+
const expectedNumber = index + 1;
|
|
485
|
+
if (scope.number !== expectedNumber) {
|
|
486
|
+
errors.push(`\`## Execution Queue\` scope numbering must start at 1 and remain contiguous; expected Scope ${expectedNumber} but found Scope ${scope.number}.`);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function validateScopeBullets(scope, errors) {
|
|
491
|
+
const presentLabels = new Set();
|
|
492
|
+
for (const line of scope.body) {
|
|
493
|
+
const bulletMatch = line.trim().match(/^- ([^:]+):/);
|
|
494
|
+
if (bulletMatch === null) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
presentLabels.add(bulletMatch[1].trim().toLowerCase());
|
|
498
|
+
}
|
|
499
|
+
for (const label of REQUIRED_SCOPE_BULLETS) {
|
|
500
|
+
if (!presentLabels.has(label)) {
|
|
501
|
+
errors.push(`Scope ${scope.number} is missing required bullet \`- ${toDisplayLabel(label)}:\`.`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function validateRecurringScopeBullets(scope, errors) {
|
|
506
|
+
const presentLabels = new Set();
|
|
507
|
+
for (const line of scope.body) {
|
|
508
|
+
const bulletMatch = line.trim().match(/^- ([^:]+):/);
|
|
509
|
+
if (bulletMatch === null) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
presentLabels.add(bulletMatch[1].trim().toLowerCase());
|
|
513
|
+
}
|
|
514
|
+
for (const label of REQUIRED_SCOPE_BULLETS) {
|
|
515
|
+
if (!presentLabels.has(label)) {
|
|
516
|
+
errors.push(`Recurring scope is missing required bullet \`- ${toDisplayLabel(label)}:\`.`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function toDisplayLabel(label) {
|
|
521
|
+
if (label === 'success condition') {
|
|
522
|
+
return 'Success Condition';
|
|
523
|
+
}
|
|
524
|
+
return `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
|
|
525
|
+
}
|