@pareto4/pp-core 1.19.0 → 1.21.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/.claude-plugin/plugin.json +1 -1
- package/agents/pp-nyquist-auditor.md +19 -14
- package/agents/pp-phase-researcher.md +10 -0
- package/agents/pp-plan-checker.md +59 -36
- package/agents/pp-planner.md +49 -53
- package/agents/pp-vault-reviewer.md +2 -0
- package/agents/pp-verifier.md +15 -60
- package/commands/pp-autonomous.md +2 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-plan-review-convergence.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +6 -3
- package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
- package/pareto-skills/pp-project-setup/SKILL.md +18 -343
- package/pp-core/bin/lib/capability-activation.cjs +77 -0
- package/pp-core/bin/lib/capability-registry.cjs +15 -15
- package/pp-core/bin/lib/capability-state.cjs +8 -18
- package/pp-core/bin/lib/ceremony.cjs +1342 -0
- package/pp-core/bin/lib/check-command-router.cjs +62 -1
- package/pp-core/bin/lib/commands.cjs +29 -3
- package/pp-core/bin/lib/config.cjs +5 -0
- package/pp-core/bin/lib/discovery-access.cjs +276 -0
- package/pp-core/bin/lib/discovery-charter.cjs +664 -0
- package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
- package/pp-core/bin/lib/discovery-entities.cjs +323 -0
- package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
- package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
- package/pp-core/bin/lib/discovery-migration.cjs +246 -0
- package/pp-core/bin/lib/discovery-phases.cjs +233 -0
- package/pp-core/bin/lib/discovery-processes.cjs +355 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
- package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
- package/pp-core/bin/lib/discovery-rules.cjs +216 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1 -1
- package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
- package/pp-core/bin/lib/discovery-stories.cjs +273 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
- package/pp-core/bin/lib/discovery-table.cjs +55 -0
- package/pp-core/bin/lib/init.cjs +50 -1
- package/pp-core/bin/lib/io.cjs +24 -0
- package/pp-core/bin/lib/loop-resolver.cjs +26 -12
- package/pp-core/bin/lib/phase.cjs +39 -3
- package/pp-core/bin/lib/plan-sizing.cjs +346 -0
- package/pp-core/bin/lib/test-substance.cjs +3190 -0
- package/pp-core/bin/lib/timeline.cjs +605 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
- package/pp-core/bin/pp-tools.cjs +214 -5
- package/pp-core/bin/shared/ceremony-gates.json +17 -0
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/ceremony.md +81 -0
- package/pp-core/references/discovery-access.md +112 -0
- package/pp-core/references/discovery-build-consumption.md +256 -0
- package/pp-core/references/discovery-charter.md +127 -0
- package/pp-core/references/discovery-data-model.md +145 -0
- package/pp-core/references/discovery-id-conventions.md +50 -14
- package/pp-core/references/discovery-integrations.md +151 -0
- package/pp-core/references/discovery-processes.md +190 -0
- package/pp-core/references/discovery-rebaseline.md +2 -2
- package/pp-core/references/discovery-research-track.md +5 -6
- package/pp-core/references/discovery-session-output.md +70 -0
- package/pp-core/references/discovery-standards.md +107 -0
- package/pp-core/references/discovery-traceability.md +2 -2
- package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
- package/pp-core/references/phase-timeline.md +107 -0
- package/pp-core/references/planner-antipatterns.md +32 -0
- package/pp-core/references/planner-discovery-extract.md +35 -0
- package/pp-core/references/planner-guidance.md +17 -14
- package/pp-core/references/planner-revision.md +1 -1
- package/pp-core/references/planning-config.md +3 -2
- package/pp-core/references/test-substance.md +324 -0
- package/pp-core/references/verifier-discovery-conformance.md +36 -0
- package/pp-core/references/verifier-stub-patterns.md +51 -0
- package/pp-core/templates/VALIDATION.md +1 -0
- package/pp-core/templates/phase-prompt.md +19 -7
- package/pp-core/templates/project-charter.md +161 -100
- package/pp-core/templates/project.md +3 -3
- package/pp-core/templates/requirements.md +18 -0
- package/pp-core/templates/roadmap.md +1 -1
- package/pp-core/templates/verification-report.md +17 -1
- package/pp-core/workflows/audit-milestone.md +4 -1
- package/pp-core/workflows/autonomous.md +11 -8
- package/pp-core/workflows/code-review.md +15 -6
- package/pp-core/workflows/discovery-sprint/charter.md +114 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
- package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
- package/pp-core/workflows/discovery-sprint.md +12 -15
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
- package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
- package/pp-core/workflows/discovery-to-build.md +70 -34
- package/pp-core/workflows/discuss-phase/discovery.md +83 -0
- package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
- package/pp-core/workflows/discuss-phase.md +10 -7
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
- package/pp-core/workflows/execute-phase.md +14 -3
- package/pp-core/workflows/new-project.md +3 -3
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
- package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
- package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
- package/pp-core/workflows/plan-phase.md +62 -124
- package/pp-core/workflows/plan-review-convergence.md +6 -3
- package/pp-core/workflows/ship.md +2 -2
- package/pp-core/workflows/stats.md +19 -4
- package/pp-core/workflows/validate-phase.md +19 -4
- package/pp-core/workflows/vault-review.md +5 -0
- package/pp-core/workflows/verify-phase.md +1 -1
- package/pp-core/workflows/verify-work.md +1 -1
- package/scripts/gen-capability-registry.cjs +43 -45
- package/scripts/run-tests.cjs +54 -30
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase Timeline: run records for the orchestrator `Agent()` spawns (Phase 35).
|
|
4
|
+
*
|
|
5
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
6
|
+
* compiles to the gitignored `pp-core/bin/lib/timeline.cjs`; do NOT hand-edit
|
|
7
|
+
* the compiled output.
|
|
8
|
+
*
|
|
9
|
+
* Record store (D-01, D-02): `{phase_dir}/{padded}-TIMELINE.jsonl`, one JSON
|
|
10
|
+
* object per line, append-only. `start` appends a full record with null end
|
|
11
|
+
* fields; `end` appends a second complete line carrying the same `id`. The
|
|
12
|
+
* current state of a run is the LAST line with its id (D-06), so nothing is
|
|
13
|
+
* ever rewritten and a torn final line costs one record at most.
|
|
14
|
+
*
|
|
15
|
+
* Only the orchestrator calls this verb, on its own checkout, after `Agent()`
|
|
16
|
+
* returns (D-03). Time comes from `realClock` so `GSD_TEST_MODE` +
|
|
17
|
+
* `GSD_NOW_MS` make every test deterministic.
|
|
18
|
+
*
|
|
19
|
+
* Subcommands: `start <role> <phase>`, `end <role> <phase> <outcome>` and
|
|
20
|
+
* `summary <phase> [--banner]`.
|
|
21
|
+
*/
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ROLE_SOURCE = exports.ROLE_AGENT = exports.TIMELINE_SOURCES = exports.TIMELINE_ROLES = void 0;
|
|
27
|
+
exports.timelineFileName = timelineFileName;
|
|
28
|
+
exports.foldRecords = foldRecords;
|
|
29
|
+
exports.formatDuration = formatDuration;
|
|
30
|
+
exports.buildPhaseTimeline = buildPhaseTimeline;
|
|
31
|
+
exports.renderBanner = renderBanner;
|
|
32
|
+
exports.cmdTimeline = cmdTimeline;
|
|
33
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
34
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
const io = require("./io.cjs");
|
|
37
|
+
const { output, ERROR_REASON } = io;
|
|
38
|
+
/** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
|
|
39
|
+
function error(message, reason) {
|
|
40
|
+
return io.error(message, reason);
|
|
41
|
+
}
|
|
42
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const phaseScopeMod = require("./phase-scope.cjs");
|
|
45
|
+
const { resolvePhaseForRead, resolvePhaseForWrite } = phaseScopeMod;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
48
|
+
const { extractPhaseToken } = phaseIdMod;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const modelResolverMod = require("./model-resolver.cjs");
|
|
51
|
+
const { resolveModelInternal } = modelResolverMod;
|
|
52
|
+
const security_cjs_1 = require("./security.cjs");
|
|
53
|
+
/** D-02: the eight roles. `other` carries an explicit agent and source. */
|
|
54
|
+
exports.TIMELINE_ROLES = Object.freeze([
|
|
55
|
+
'researcher', 'pattern-mapper', 'planner', 'plan-checker',
|
|
56
|
+
'executor', 'code-reviewer', 'verifier', 'other',
|
|
57
|
+
]);
|
|
58
|
+
exports.TIMELINE_SOURCES = Object.freeze([
|
|
59
|
+
'plan-phase', 'execute-phase', 'code-review',
|
|
60
|
+
]);
|
|
61
|
+
/** D-08: the agent a role runs as when `--agent` is omitted (no entry for `other`). */
|
|
62
|
+
exports.ROLE_AGENT = Object.freeze({
|
|
63
|
+
'researcher': 'pp-phase-researcher',
|
|
64
|
+
'pattern-mapper': 'pp-pattern-mapper',
|
|
65
|
+
'planner': 'pp-planner',
|
|
66
|
+
'plan-checker': 'pp-plan-checker',
|
|
67
|
+
'executor': 'pp-executor',
|
|
68
|
+
'code-reviewer': 'pp-code-reviewer',
|
|
69
|
+
'verifier': 'pp-verifier',
|
|
70
|
+
});
|
|
71
|
+
/** D-08: the workflow a role belongs to when `--source` is omitted (no entry for `other`). */
|
|
72
|
+
exports.ROLE_SOURCE = Object.freeze({
|
|
73
|
+
'researcher': 'plan-phase',
|
|
74
|
+
'pattern-mapper': 'plan-phase',
|
|
75
|
+
'planner': 'plan-phase',
|
|
76
|
+
'plan-checker': 'plan-phase',
|
|
77
|
+
'executor': 'execute-phase',
|
|
78
|
+
'verifier': 'execute-phase',
|
|
79
|
+
'code-reviewer': 'code-review',
|
|
80
|
+
});
|
|
81
|
+
const PLAN_RE = /^[A-Za-z0-9_.-]{1,40}$/;
|
|
82
|
+
const MAX_LINE_CHARS = 4096;
|
|
83
|
+
const MAX_TEXT = 80;
|
|
84
|
+
const ID_RE = /^(.+?)-(researcher|pattern-mapper|planner|plan-checker|executor|code-reviewer|verifier|other)(?:-(.+))?-(\d+)$/;
|
|
85
|
+
const VALUE_FLAGS = new Set(['plan', 'agent', 'model', 'source', 'id', 'phase', 'role', 'outcome']);
|
|
86
|
+
function isRole(value) {
|
|
87
|
+
return typeof value === 'string' && exports.TIMELINE_ROLES.includes(value);
|
|
88
|
+
}
|
|
89
|
+
function isSource(value) {
|
|
90
|
+
return typeof value === 'string' && exports.TIMELINE_SOURCES.includes(value);
|
|
91
|
+
}
|
|
92
|
+
/** `35-phase-timeline` -> `35-TIMELINE.jsonl` (D-01). */
|
|
93
|
+
function timelineFileName(phaseDirName) {
|
|
94
|
+
return `${extractPhaseToken(phaseDirName) || phaseDirName}-TIMELINE.jsonl`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Free text becomes one short line (T-35-01): prompt-injection markers are
|
|
98
|
+
* neutralised, control characters and `|` (the table delimiter) become spaces,
|
|
99
|
+
* whitespace collapses, length is capped. `safeAlphabet` additionally drops
|
|
100
|
+
* everything outside the agent/model alphabet.
|
|
101
|
+
*/
|
|
102
|
+
function cleanText(value, max = MAX_TEXT, safeAlphabet = false) {
|
|
103
|
+
const raw = typeof value === 'string' ? value : '';
|
|
104
|
+
let text = (0, security_cjs_1.sanitizeForPrompt)(raw) || '';
|
|
105
|
+
text = text.replace(/[\u0000-\u001f\u007f|]/g, ' ');
|
|
106
|
+
if (safeAlphabet)
|
|
107
|
+
text = text.replace(/[^A-Za-z0-9_.:@/[\] -]/g, '');
|
|
108
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, max).trim();
|
|
109
|
+
}
|
|
110
|
+
function escapeRegex(text) {
|
|
111
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Turn one parsed JSON value into a record, or null when it fails the shape
|
|
115
|
+
* check (T-35-03). `duration_ms` is always recomputed from the two timestamps
|
|
116
|
+
* and clamped at 0 (T-35-04); a stored value is ignored. An `ended_at` that
|
|
117
|
+
* Date.parse rejects is dropped, which makes the run incomplete. Both
|
|
118
|
+
* timestamps are re-emitted in canonical ISO form, never as the stored text:
|
|
119
|
+
* `Date.parse` accepts strings the sanitizer would reject (parenthesised
|
|
120
|
+
* comments, for one), and the raw value would otherwise flow into `summary`,
|
|
121
|
+
* `stats.json` and the closing line `end` writes (T-35-03).
|
|
122
|
+
*/
|
|
123
|
+
function normalizeRecord(value) {
|
|
124
|
+
if (!value || typeof value !== 'object')
|
|
125
|
+
return null;
|
|
126
|
+
const r = value;
|
|
127
|
+
if (typeof r['id'] !== 'string' || r['id'].length === 0 || r['id'].length > 160)
|
|
128
|
+
return null;
|
|
129
|
+
if (!isRole(r['role']))
|
|
130
|
+
return null;
|
|
131
|
+
if (typeof r['started_at'] !== 'string')
|
|
132
|
+
return null;
|
|
133
|
+
const startMs = Date.parse(r['started_at']);
|
|
134
|
+
if (!Number.isFinite(startMs))
|
|
135
|
+
return null;
|
|
136
|
+
const startedAt = new Date(startMs).toISOString();
|
|
137
|
+
const role = r['role'];
|
|
138
|
+
let endedAt = null;
|
|
139
|
+
let durationMs = null;
|
|
140
|
+
if (typeof r['ended_at'] === 'string') {
|
|
141
|
+
const endMs = Date.parse(r['ended_at']);
|
|
142
|
+
if (Number.isFinite(endMs)) {
|
|
143
|
+
endedAt = new Date(endMs).toISOString();
|
|
144
|
+
durationMs = Math.max(0, endMs - startMs);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const plan = typeof r['plan'] === 'string' && PLAN_RE.test(r['plan']) ? r['plan'] : null;
|
|
148
|
+
const outcome = typeof r['outcome'] === 'string' ? cleanText(r['outcome']) : null;
|
|
149
|
+
return {
|
|
150
|
+
id: r['id'],
|
|
151
|
+
phase: typeof r['phase'] === 'string' ? cleanText(r['phase'], 40, true) : '',
|
|
152
|
+
agent: cleanText(r['agent'], MAX_TEXT, true) || exports.ROLE_AGENT[role] || 'unknown',
|
|
153
|
+
role,
|
|
154
|
+
model: cleanText(r['model'], MAX_TEXT, true) || 'unknown',
|
|
155
|
+
plan,
|
|
156
|
+
started_at: startedAt,
|
|
157
|
+
ended_at: endedAt,
|
|
158
|
+
duration_ms: durationMs,
|
|
159
|
+
outcome,
|
|
160
|
+
source: isSource(r['source']) ? r['source'] : (exports.ROLE_SOURCE[role] ?? 'execute-phase'),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Every valid line of a file's text, in file order. Bad lines are skipped, never thrown on. */
|
|
164
|
+
function parseEvents(text) {
|
|
165
|
+
const events = [];
|
|
166
|
+
for (const line of text.split('\n')) {
|
|
167
|
+
if (!line.trim() || line.length > MAX_LINE_CHARS)
|
|
168
|
+
continue;
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(line);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
continue; // torn or foreign line
|
|
175
|
+
}
|
|
176
|
+
const record = normalizeRecord(parsed);
|
|
177
|
+
if (record)
|
|
178
|
+
events.push(record);
|
|
179
|
+
}
|
|
180
|
+
return events;
|
|
181
|
+
}
|
|
182
|
+
function readEvents(file) {
|
|
183
|
+
let text;
|
|
184
|
+
try {
|
|
185
|
+
text = node_fs_1.default.readFileSync(file, 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return parseEvents(text);
|
|
191
|
+
}
|
|
192
|
+
/** D-06: the current state of each run is its last line. */
|
|
193
|
+
function currentStates(events) {
|
|
194
|
+
const states = new Map();
|
|
195
|
+
events.forEach((record, index) => {
|
|
196
|
+
const prior = states.get(record.id);
|
|
197
|
+
states.set(record.id, { current: record, seq: prior ? prior.seq : index });
|
|
198
|
+
});
|
|
199
|
+
return states;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* D-11: every run of a file's text, one per id (last line wins, D-06), sorted
|
|
203
|
+
* by `started_at` ascending with ties in file order. Torn and foreign lines are
|
|
204
|
+
* skipped, so a damaged file degrades to fewer runs instead of an error.
|
|
205
|
+
*/
|
|
206
|
+
function foldRecords(text) {
|
|
207
|
+
const states = [...currentStates(parseEvents(text)).values()];
|
|
208
|
+
states.sort((a, b) => {
|
|
209
|
+
const delta = Date.parse(a.current.started_at) - Date.parse(b.current.started_at);
|
|
210
|
+
return delta !== 0 ? delta : a.seq - b.seq;
|
|
211
|
+
});
|
|
212
|
+
return states.map((state) => state.current);
|
|
213
|
+
}
|
|
214
|
+
/** D-10: `38s`, `45m 10s`, `1h 12m`. Floors; negative and non-finite input is `0s`. */
|
|
215
|
+
function formatDuration(ms) {
|
|
216
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
217
|
+
return '0s';
|
|
218
|
+
const total = Math.floor(ms / 1000);
|
|
219
|
+
const hours = Math.floor(total / 3600);
|
|
220
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
221
|
+
const seconds = total % 60;
|
|
222
|
+
if (hours >= 1)
|
|
223
|
+
return `${hours}h ${String(minutes).padStart(2, '0')}m`;
|
|
224
|
+
if (minutes >= 1)
|
|
225
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
226
|
+
return `${seconds}s`;
|
|
227
|
+
}
|
|
228
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
229
|
+
const localDate = (ms) => {
|
|
230
|
+
const d = new Date(ms);
|
|
231
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
232
|
+
};
|
|
233
|
+
const localClock = (ms) => {
|
|
234
|
+
const d = new Date(ms);
|
|
235
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
236
|
+
};
|
|
237
|
+
/** max(end) - min(start) over the intervals, or null for an empty list. */
|
|
238
|
+
function spanOf(intervals) {
|
|
239
|
+
if (intervals.length === 0)
|
|
240
|
+
return null;
|
|
241
|
+
let start = Infinity;
|
|
242
|
+
let end = -Infinity;
|
|
243
|
+
for (const i of intervals) {
|
|
244
|
+
if (i.start < start)
|
|
245
|
+
start = i.start;
|
|
246
|
+
if (i.end > end)
|
|
247
|
+
end = i.end;
|
|
248
|
+
}
|
|
249
|
+
return { start, end: Math.max(start, end) };
|
|
250
|
+
}
|
|
251
|
+
function labelOf(role, plan, model, duration) {
|
|
252
|
+
return `${role}${plan ? ' ' + plan : ''}${model && model !== 'inherit' ? ` (${model})` : ''} ${duration}`;
|
|
253
|
+
}
|
|
254
|
+
const runsLabel = (n) => (n === 1 ? '1 run' : `${n} runs`);
|
|
255
|
+
/**
|
|
256
|
+
* D-04, D-10, D-11, D-13: fold records into the numbers every reader shows.
|
|
257
|
+
*
|
|
258
|
+
* Wall-clock interpretation (D-13): each source (plan-phase, execute-phase,
|
|
259
|
+
* code-review) gets one span, max(ended_at) - min(started_at) over its
|
|
260
|
+
* completed runs, so parallel executors count once and idle days between
|
|
261
|
+
* planning and executing drop out. The headline total is the length of the
|
|
262
|
+
* UNION of those spans: it equals the sum of the spans whenever they are
|
|
263
|
+
* disjoint, and a code review that runs inside the execute-phase span (the
|
|
264
|
+
* default flow: execute:post runs the review before the verifier) is counted
|
|
265
|
+
* once instead of twice. `elapsed` is the raw first-start to last-end span and
|
|
266
|
+
* agent time is the sum of completed durations. Orphans (no ended_at) are
|
|
267
|
+
* counted in `incomplete_count` and excluded from all three sums.
|
|
268
|
+
*/
|
|
269
|
+
function buildPhaseTimeline(records) {
|
|
270
|
+
const completed = records.filter((r) => r.ended_at !== null);
|
|
271
|
+
const intervalOf = (r) => {
|
|
272
|
+
const start = Date.parse(r.started_at);
|
|
273
|
+
return { start, end: start + (r.duration_ms ?? 0) };
|
|
274
|
+
};
|
|
275
|
+
const bySource = { 'plan-phase': 0, 'execute-phase': 0, 'code-review': 0 };
|
|
276
|
+
const spans = [];
|
|
277
|
+
for (const source of exports.TIMELINE_SOURCES) {
|
|
278
|
+
const span = spanOf(completed.filter((r) => r.source === source).map(intervalOf));
|
|
279
|
+
if (!span)
|
|
280
|
+
continue;
|
|
281
|
+
bySource[source] = span.end - span.start;
|
|
282
|
+
if (span.end > span.start)
|
|
283
|
+
spans.push(span);
|
|
284
|
+
}
|
|
285
|
+
spans.sort((a, b) => a.start - b.start);
|
|
286
|
+
let wallClockMs = 0;
|
|
287
|
+
let open = null;
|
|
288
|
+
for (const span of spans) {
|
|
289
|
+
if (open && span.start <= open.end) {
|
|
290
|
+
open = { start: open.start, end: Math.max(open.end, span.end) };
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
if (open)
|
|
294
|
+
wallClockMs += open.end - open.start;
|
|
295
|
+
open = { ...span };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (open)
|
|
299
|
+
wallClockMs += open.end - open.start;
|
|
300
|
+
const all = spanOf(completed.map(intervalOf));
|
|
301
|
+
const elapsedMs = all ? all.end - all.start : 0;
|
|
302
|
+
const agentTimeMs = completed.reduce((sum, r) => sum + (r.duration_ms ?? 0), 0);
|
|
303
|
+
const firstStart = records.length > 0 ? Date.parse(records[0].started_at) : null;
|
|
304
|
+
const date = firstStart === null ? null : localDate(firstStart);
|
|
305
|
+
const runs = records.map((r) => {
|
|
306
|
+
const startMs = Date.parse(r.started_at);
|
|
307
|
+
const done = r.ended_at !== null;
|
|
308
|
+
return {
|
|
309
|
+
...r,
|
|
310
|
+
display: {
|
|
311
|
+
started: date !== null && localDate(startMs) !== date ? `${localDate(startMs).slice(5)} ${localClock(startMs)}` : localClock(startMs),
|
|
312
|
+
duration: done ? formatDuration(r.duration_ms ?? 0) : 'incomplete',
|
|
313
|
+
outcome: done ? (r.outcome ?? '') : 'incomplete',
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
const longest = completed
|
|
318
|
+
.map((r, index) => ({ r, index }))
|
|
319
|
+
.sort((a, b) => {
|
|
320
|
+
const byDuration = (b.r.duration_ms ?? 0) - (a.r.duration_ms ?? 0);
|
|
321
|
+
if (byDuration !== 0)
|
|
322
|
+
return byDuration;
|
|
323
|
+
const byStart = Date.parse(a.r.started_at) - Date.parse(b.r.started_at);
|
|
324
|
+
return byStart !== 0 ? byStart : a.index - b.index;
|
|
325
|
+
})
|
|
326
|
+
.slice(0, 3)
|
|
327
|
+
.map(({ r }) => {
|
|
328
|
+
const duration = formatDuration(r.duration_ms ?? 0);
|
|
329
|
+
return {
|
|
330
|
+
role: r.role,
|
|
331
|
+
plan: r.plan,
|
|
332
|
+
model: r.model,
|
|
333
|
+
duration_ms: r.duration_ms ?? 0,
|
|
334
|
+
duration,
|
|
335
|
+
label: labelOf(r.role, r.plan, r.model, duration),
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
const incompleteCount = records.length - completed.length;
|
|
339
|
+
let footer = `Total wall-clock: ${formatDuration(wallClockMs)} \u00b7 agent time ${formatDuration(agentTimeMs)} \u00b7 ${runsLabel(records.length)}`;
|
|
340
|
+
if (incompleteCount > 0)
|
|
341
|
+
footer += ` (${incompleteCount} incomplete)`;
|
|
342
|
+
if (elapsedMs !== wallClockMs)
|
|
343
|
+
footer += ` \u00b7 elapsed ${formatDuration(elapsedMs)}`;
|
|
344
|
+
return {
|
|
345
|
+
runs,
|
|
346
|
+
run_count: records.length,
|
|
347
|
+
incomplete_count: incompleteCount,
|
|
348
|
+
wall_clock_ms: wallClockMs,
|
|
349
|
+
wall_clock: formatDuration(wallClockMs),
|
|
350
|
+
elapsed_ms: elapsedMs,
|
|
351
|
+
elapsed: formatDuration(elapsedMs),
|
|
352
|
+
agent_time_ms: agentTimeMs,
|
|
353
|
+
agent_time: formatDuration(agentTimeMs),
|
|
354
|
+
by_source: bySource,
|
|
355
|
+
longest,
|
|
356
|
+
date,
|
|
357
|
+
footer,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** D-14, D-15: the two banner lines, or '' when nothing completed. */
|
|
361
|
+
function renderBanner(t) {
|
|
362
|
+
if (t.longest.length === 0)
|
|
363
|
+
return '';
|
|
364
|
+
return `Wall-clock: ${t.wall_clock} (${runsLabel(t.run_count)})\nLongest: ${t.longest.map((l) => l.label).join(' \u00b7 ')}`;
|
|
365
|
+
}
|
|
366
|
+
function parseArgs(argv) {
|
|
367
|
+
const positionals = [];
|
|
368
|
+
const flags = {};
|
|
369
|
+
for (let i = 0; i < argv.length; i++) {
|
|
370
|
+
const arg = argv[i];
|
|
371
|
+
if (arg === '--banner') {
|
|
372
|
+
flags['banner'] = '1';
|
|
373
|
+
}
|
|
374
|
+
else if (arg.startsWith('--')) {
|
|
375
|
+
const name = arg.slice(2);
|
|
376
|
+
if (!VALUE_FLAGS.has(name))
|
|
377
|
+
error(`Unknown timeline flag: ${arg}`, ERROR_REASON.USAGE);
|
|
378
|
+
const next = argv[i + 1];
|
|
379
|
+
if (next === undefined)
|
|
380
|
+
error(`Missing value for ${arg}`, ERROR_REASON.USAGE);
|
|
381
|
+
flags[name] = next;
|
|
382
|
+
i++;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
positionals.push(arg);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { positionals, flags };
|
|
389
|
+
}
|
|
390
|
+
function requireRole(value) {
|
|
391
|
+
if (!isRole(value)) {
|
|
392
|
+
error(`Unknown role: ${String(value)}. Expected one of: ${exports.TIMELINE_ROLES.join(', ')}`, ERROR_REASON.USAGE);
|
|
393
|
+
}
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
function requirePlan(value) {
|
|
397
|
+
if (value === undefined)
|
|
398
|
+
return null;
|
|
399
|
+
if (!PLAN_RE.test(value))
|
|
400
|
+
error('--plan must match ^[A-Za-z0-9_.-]{1,40}$', ERROR_REASON.USAGE);
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
/** The live phase directory a record may be written into (T-35-02, T-35-05). */
|
|
404
|
+
function resolveWriteTarget(cwd, phase) {
|
|
405
|
+
if (phase === undefined || phase === '')
|
|
406
|
+
error('Missing phase argument', ERROR_REASON.USAGE);
|
|
407
|
+
const verdict = resolvePhaseForWrite(cwd, phase);
|
|
408
|
+
if (!verdict.ok) {
|
|
409
|
+
if (verdict.reason === 'archived') {
|
|
410
|
+
error(`Phase ${phase} is archived; timeline records are written to live phases only`, ERROR_REASON.PHASE_NOT_FOUND);
|
|
411
|
+
}
|
|
412
|
+
error(`Phase ${phase} directory not found`, ERROR_REASON.PHASE_NOT_FOUND);
|
|
413
|
+
}
|
|
414
|
+
const dir = node_path_1.default.join(cwd, verdict.match.directory);
|
|
415
|
+
const dirName = node_path_1.default.basename(verdict.match.directory);
|
|
416
|
+
return { token: extractPhaseToken(dirName) || dirName, file: node_path_1.default.join(dir, timelineFileName(dirName)) };
|
|
417
|
+
}
|
|
418
|
+
function cmdStart(cwd, argv, raw) {
|
|
419
|
+
const { positionals, flags } = parseArgs(argv);
|
|
420
|
+
const role = requireRole(flags['role'] ?? positionals[0]);
|
|
421
|
+
const phase = flags['phase'] ?? positionals[1];
|
|
422
|
+
const plan = requirePlan(flags['plan']);
|
|
423
|
+
if (flags['source'] !== undefined && !isSource(flags['source'])) {
|
|
424
|
+
error(`Unknown source: ${flags['source']}. Expected one of: ${exports.TIMELINE_SOURCES.join(', ')}`, ERROR_REASON.USAGE);
|
|
425
|
+
}
|
|
426
|
+
if (role === 'other' && (flags['agent'] === undefined || flags['source'] === undefined)) {
|
|
427
|
+
error('Role "other" requires --agent and --source', ERROR_REASON.USAGE);
|
|
428
|
+
}
|
|
429
|
+
const agent = cleanText(flags['agent'], MAX_TEXT, true) || exports.ROLE_AGENT[role];
|
|
430
|
+
if (!agent)
|
|
431
|
+
error('Role "other" requires a non-empty --agent', ERROR_REASON.USAGE);
|
|
432
|
+
const source = isSource(flags['source']) ? flags['source'] : exports.ROLE_SOURCE[role];
|
|
433
|
+
if (!source)
|
|
434
|
+
error('Role "other" requires --source', ERROR_REASON.USAGE);
|
|
435
|
+
const { token, file } = resolveWriteTarget(cwd, phase);
|
|
436
|
+
let model = cleanText(flags['model'], MAX_TEXT, true);
|
|
437
|
+
if (!model) {
|
|
438
|
+
try {
|
|
439
|
+
model = cleanText(resolveModelInternal(cwd, agent), MAX_TEXT, true);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
model = '';
|
|
443
|
+
}
|
|
444
|
+
if (!model)
|
|
445
|
+
model = 'unknown';
|
|
446
|
+
}
|
|
447
|
+
const prefix = `${token}-${role}${plan ? '-' + plan : ''}-`;
|
|
448
|
+
const siblings = new RegExp(`^${escapeRegex(prefix)}\\d+$`);
|
|
449
|
+
const existing = new Set();
|
|
450
|
+
for (const record of readEvents(file)) {
|
|
451
|
+
if (siblings.test(record.id))
|
|
452
|
+
existing.add(record.id);
|
|
453
|
+
}
|
|
454
|
+
const id = `${prefix}${existing.size + 1}`;
|
|
455
|
+
const record = {
|
|
456
|
+
id,
|
|
457
|
+
phase: token,
|
|
458
|
+
agent,
|
|
459
|
+
role,
|
|
460
|
+
model,
|
|
461
|
+
plan,
|
|
462
|
+
started_at: clock_cjs_1.realClock.nowIso(),
|
|
463
|
+
ended_at: null,
|
|
464
|
+
duration_ms: null,
|
|
465
|
+
outcome: null,
|
|
466
|
+
source,
|
|
467
|
+
};
|
|
468
|
+
node_fs_1.default.appendFileSync(file, JSON.stringify(record) + '\n');
|
|
469
|
+
output(record, raw, record.id);
|
|
470
|
+
}
|
|
471
|
+
function cmdEnd(cwd, argv, raw) {
|
|
472
|
+
const { positionals, flags } = parseArgs(argv);
|
|
473
|
+
let id;
|
|
474
|
+
let role;
|
|
475
|
+
let phase;
|
|
476
|
+
let outcomeText;
|
|
477
|
+
if (flags['id'] !== undefined) {
|
|
478
|
+
id = flags['id'];
|
|
479
|
+
const derived = ID_RE.exec(id);
|
|
480
|
+
phase = flags['phase'] ?? positionals[1] ?? (derived ? derived[1] : undefined);
|
|
481
|
+
outcomeText = flags['outcome'] ?? (positionals.length >= 3 ? positionals[2] : positionals.length === 1 ? positionals[0] : undefined);
|
|
482
|
+
}
|
|
483
|
+
else if (positionals.length === 1 && flags['outcome'] !== undefined && ID_RE.test(positionals[0])) {
|
|
484
|
+
id = positionals[0];
|
|
485
|
+
const derived = ID_RE.exec(id);
|
|
486
|
+
phase = flags['phase'] ?? (derived ? derived[1] : undefined);
|
|
487
|
+
outcomeText = flags['outcome'];
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
role = requireRole(flags['role'] ?? positionals[0]);
|
|
491
|
+
phase = flags['phase'] ?? positionals[1];
|
|
492
|
+
outcomeText = flags['outcome'] ?? positionals[2];
|
|
493
|
+
}
|
|
494
|
+
if (outcomeText === undefined || cleanText(outcomeText) === '')
|
|
495
|
+
error('Missing outcome', ERROR_REASON.USAGE);
|
|
496
|
+
const outcome = cleanText(outcomeText);
|
|
497
|
+
const plan = requirePlan(flags['plan']);
|
|
498
|
+
const { file } = resolveWriteTarget(cwd, phase);
|
|
499
|
+
const states = currentStates(readEvents(file));
|
|
500
|
+
let target;
|
|
501
|
+
if (id !== undefined) {
|
|
502
|
+
const state = states.get(id);
|
|
503
|
+
if (state && state.current.ended_at === null)
|
|
504
|
+
target = state.current;
|
|
505
|
+
if (!target)
|
|
506
|
+
error(`No open timeline run with id ${id}`, ERROR_REASON.TIMELINE_NO_OPEN_RUN);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
let best;
|
|
510
|
+
for (const state of states.values()) {
|
|
511
|
+
const c = state.current;
|
|
512
|
+
if (c.ended_at !== null || c.role !== role)
|
|
513
|
+
continue;
|
|
514
|
+
if (plan !== null && c.plan !== plan)
|
|
515
|
+
continue;
|
|
516
|
+
if (!best) {
|
|
517
|
+
best = state;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const a = Date.parse(c.started_at);
|
|
521
|
+
const b = Date.parse(best.current.started_at);
|
|
522
|
+
if (a > b || (a === b && state.seq > best.seq))
|
|
523
|
+
best = state;
|
|
524
|
+
}
|
|
525
|
+
if (!best) {
|
|
526
|
+
error(`No open timeline run for role ${String(role)} in phase ${String(phase)}${plan ? ` (plan ${plan})` : ''}`, ERROR_REASON.TIMELINE_NO_OPEN_RUN);
|
|
527
|
+
}
|
|
528
|
+
target = best.current;
|
|
529
|
+
}
|
|
530
|
+
const endedAt = clock_cjs_1.realClock.nowIso();
|
|
531
|
+
const closed = {
|
|
532
|
+
id: target.id,
|
|
533
|
+
phase: target.phase,
|
|
534
|
+
agent: target.agent,
|
|
535
|
+
role: target.role,
|
|
536
|
+
model: target.model,
|
|
537
|
+
plan: target.plan,
|
|
538
|
+
started_at: target.started_at,
|
|
539
|
+
ended_at: endedAt,
|
|
540
|
+
duration_ms: Math.max(0, Date.parse(endedAt) - Date.parse(target.started_at)),
|
|
541
|
+
outcome,
|
|
542
|
+
source: target.source,
|
|
543
|
+
};
|
|
544
|
+
node_fs_1.default.appendFileSync(file, JSON.stringify(closed) + '\n');
|
|
545
|
+
output(closed, raw, closed.id);
|
|
546
|
+
}
|
|
547
|
+
function cmdSummary(cwd, argv, raw) {
|
|
548
|
+
const { positionals, flags } = parseArgs(argv);
|
|
549
|
+
const phase = flags['phase'] ?? positionals[0];
|
|
550
|
+
const banner = flags['banner'] !== undefined;
|
|
551
|
+
if (phase === undefined || phase === '')
|
|
552
|
+
error('Missing phase argument', ERROR_REASON.USAGE);
|
|
553
|
+
// Readers may use a live or an archived phase (D-12).
|
|
554
|
+
const match = resolvePhaseForRead(cwd, phase);
|
|
555
|
+
if (!match) {
|
|
556
|
+
if (banner)
|
|
557
|
+
return; // a completion block must never carry a stray line or an error
|
|
558
|
+
error(`Phase ${phase} not found`, ERROR_REASON.PHASE_NOT_FOUND);
|
|
559
|
+
}
|
|
560
|
+
const dir = node_path_1.default.join(cwd, match.directory);
|
|
561
|
+
let texts = [];
|
|
562
|
+
try {
|
|
563
|
+
texts = node_fs_1.default.readdirSync(dir)
|
|
564
|
+
.filter((name) => name.endsWith('-TIMELINE.jsonl'))
|
|
565
|
+
.sort()
|
|
566
|
+
.map((name) => node_fs_1.default.readFileSync(node_path_1.default.join(dir, name), 'utf-8'));
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
texts = [];
|
|
570
|
+
}
|
|
571
|
+
const runs = foldRecords(texts.join('\n'));
|
|
572
|
+
if (runs.length === 0) {
|
|
573
|
+
if (banner) {
|
|
574
|
+
output({ phase, present: false }, true, '');
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
output({ phase, present: false, run_count: 0, message: 'no recorded runs' }, raw, 'no recorded runs');
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const timeline = buildPhaseTimeline(runs);
|
|
582
|
+
if (banner) {
|
|
583
|
+
output(timeline, true, renderBanner(timeline));
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
output({ phase, present: true, ...timeline }, raw, timeline.footer);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function cmdTimeline(cwd, args, raw) {
|
|
590
|
+
const sub = args[0];
|
|
591
|
+
const rest = args.slice(1);
|
|
592
|
+
switch (sub) {
|
|
593
|
+
case 'start':
|
|
594
|
+
cmdStart(cwd, rest, raw);
|
|
595
|
+
return;
|
|
596
|
+
case 'end':
|
|
597
|
+
cmdEnd(cwd, rest, raw);
|
|
598
|
+
return;
|
|
599
|
+
case 'summary':
|
|
600
|
+
cmdSummary(cwd, rest, raw);
|
|
601
|
+
return;
|
|
602
|
+
default:
|
|
603
|
+
error(`Unknown timeline subcommand: ${String(sub)}. Available: start, end, summary`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.REVIEW_FILES_CAP = exports.REVIEW_VERDICTS_HEADING = exports.REVIEW_ARTIFACT_MARKER = exports.REVIEW_ARTIFACT_SUFFIX = exports.REVIEW_VERDICTS = void 0;
|
|
6
|
+
exports.DEVIATIONS_FILE = exports.REVIEW_FILES_CAP = exports.REVIEW_VERDICTS_HEADING = exports.REVIEW_ARTIFACT_MARKER = exports.REVIEW_ARTIFACT_SUFFIX = exports.REVIEW_VERDICTS = void 0;
|
|
7
|
+
exports.readDeviations = readDeviations;
|
|
7
8
|
exports.deriveStandardsReview = deriveStandardsReview;
|
|
8
9
|
exports.formatReviewCounts = formatReviewCounts;
|
|
9
10
|
exports.validateReviewArtifact = validateReviewArtifact;
|
|
@@ -59,6 +60,8 @@ exports.REVIEW_VERDICTS_HEADING = '## Standard Verdicts';
|
|
|
59
60
|
exports.REVIEW_FILES_CAP = 200;
|
|
60
61
|
/** Bound on a git query a review makes. */
|
|
61
62
|
const REVIEW_GIT_TIMEOUT_MS = 30_000;
|
|
63
|
+
/** Where the Discovery Sprint records agreed departures from the vault. */
|
|
64
|
+
exports.DEVIATIONS_FILE = '.planning/discovery/_deviations.md';
|
|
62
65
|
// ─── Derivation ──────────────────────────────────────────────────────────────
|
|
63
66
|
const PLAN_RE = /(^|-)PLAN\.md$/;
|
|
64
67
|
const SUMMARY_RE = /(^|-)SUMMARY\.md$/;
|
|
@@ -73,6 +76,47 @@ function readBounded(abs) {
|
|
|
73
76
|
return null;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The deviations on file, read by header name so Batch 3's full grammar can
|
|
81
|
+
* add or reorder columns without touching this reader. `gap` is set when the
|
|
82
|
+
* file exists but yields no readable table with both join columns.
|
|
83
|
+
*/
|
|
84
|
+
function readDeviations(root) {
|
|
85
|
+
const abs = node_path_1.default.join(root, exports.DEVIATIONS_FILE);
|
|
86
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
87
|
+
return { deviations: [], gap: null };
|
|
88
|
+
const text = readBounded(abs);
|
|
89
|
+
if (text === null)
|
|
90
|
+
return { deviations: [], gap: `${exports.DEVIATIONS_FILE} could not be read (unreadable or over the size cap), so deviations on file are unknown` };
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
92
|
+
const table = require('./discovery-table.cjs');
|
|
93
|
+
const deviations = [];
|
|
94
|
+
let joinable = false;
|
|
95
|
+
for (const section of table.splitSections(text.split(/\r?\n/), 2)) {
|
|
96
|
+
for (const tbl of table.parseTables(section.body)) {
|
|
97
|
+
const names = tbl.header.map((h) => h.trim().toLowerCase());
|
|
98
|
+
const col = (name) => names.indexOf(name);
|
|
99
|
+
const standard = col('standard');
|
|
100
|
+
const status = col('status');
|
|
101
|
+
if (standard === -1 || status === -1)
|
|
102
|
+
continue;
|
|
103
|
+
joinable = true;
|
|
104
|
+
const cell = (row, i) => (i === -1 ? '' : (row.cells[i] ?? '').trim());
|
|
105
|
+
for (const row of tbl.rows) {
|
|
106
|
+
deviations.push({
|
|
107
|
+
id: cell(row, col('id')),
|
|
108
|
+
standard: cell(row, standard),
|
|
109
|
+
status: cell(row, status),
|
|
110
|
+
decision: cell(row, col('decision')),
|
|
111
|
+
approver: cell(row, col('approver')),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!joinable)
|
|
117
|
+
return { deviations: [], gap: `${exports.DEVIATIONS_FILE} carries no table with Standard and Status columns, so deviations on file are unknown` };
|
|
118
|
+
return { deviations, gap: null };
|
|
119
|
+
}
|
|
76
120
|
function consultedText(entry, hasSummary) {
|
|
77
121
|
if (!hasSummary)
|
|
78
122
|
return 'no summary';
|
|
@@ -206,6 +250,9 @@ function deriveStandardsReview(root, cwd, positional, opts = {}) {
|
|
|
206
250
|
});
|
|
207
251
|
}
|
|
208
252
|
}
|
|
253
|
+
const onFile = readDeviations(root);
|
|
254
|
+
if (onFile.gap !== null)
|
|
255
|
+
gaps.push(onFile.gap);
|
|
209
256
|
if (plans.length === 0 || documents.size === 0) {
|
|
210
257
|
return {
|
|
211
258
|
status: 'no-records',
|
|
@@ -216,9 +263,10 @@ function deriveStandardsReview(root, cwd, positional, opts = {}) {
|
|
|
216
263
|
plans,
|
|
217
264
|
documents: [],
|
|
218
265
|
gaps,
|
|
266
|
+
deviations: onFile.deviations,
|
|
219
267
|
};
|
|
220
268
|
}
|
|
221
|
-
return { status: 'ok', phaseDir: phase.rel, plans, documents: [...documents.values()], gaps };
|
|
269
|
+
return { status: 'ok', phaseDir: phase.rel, plans, documents: [...documents.values()], gaps, deviations: onFile.deviations };
|
|
222
270
|
}
|
|
223
271
|
/** The counts line, in verdict order: `p PASS · f FAIL · n N-A · i INCONCLUSIVE (of N documents)`. */
|
|
224
272
|
function formatReviewCounts(counts) {
|