@pareto4/pp-core 1.19.0 → 1.20.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-phase-researcher.md +10 -0
- package/agents/pp-planner.md +15 -0
- package/agents/pp-vault-reviewer.md +2 -0
- package/agents/pp-verifier.md +12 -8
- package/commands/pp-discovery.md +3 -2
- 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-registry.cjs +1 -1
- 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/io.cjs +22 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
- package/pp-core/bin/pp-tools.cjs +143 -1
- 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/planner-discovery-extract.md +35 -0
- package/pp-core/references/verifier-discovery-conformance.md +36 -0
- 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/verification-report.md +16 -0
- 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/new-project.md +3 -3
- package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
- package/pp-core/workflows/plan-phase.md +15 -10
- package/pp-core/workflows/vault-review.md +5 -0
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery project charter check (Discovery Sprint improvement plan, Batch 1).
|
|
4
|
+
*
|
|
5
|
+
* The Project Charter is the one document every stakeholder signs at the end
|
|
6
|
+
* of a Discovery Sprint. Session 13 drafts it from the session record into
|
|
7
|
+
* `docs/discovery/PROJECT-CHARTER.md` (client-visible, like `docs/architecture/`),
|
|
8
|
+
* in the sprint language, from the fixed-shape template in
|
|
9
|
+
* `pp-core/templates/project-charter.md`. This module checks the structure of
|
|
10
|
+
* that document — never its judgement:
|
|
11
|
+
*
|
|
12
|
+
* - the frontmatter carries every machine field, with `status`, `go_no_go`,
|
|
13
|
+
* `version`, `date` and `language` inside their closed grammars;
|
|
14
|
+
* - every one of the twenty sections is present exactly once, in template
|
|
15
|
+
* order, identified by its `<!-- charter:<key> -->` heading key, so the
|
|
16
|
+
* same shape is recognised whatever language the headings are rendered in;
|
|
17
|
+
* - every ID a table cell cites resolves: requirement IDs against
|
|
18
|
+
* `_requirements.md` (through the rollup parser, the owner of that file),
|
|
19
|
+
* every other discovery family (`D-`, `A-`, `OI-`, `R-`, `CON-`, `SCR-`,
|
|
20
|
+
* `E-`, `US-`, `BO-`, `ADR-`, `IF-`, `CR-`, `RS-`, `DG-`, `DEV-`) against the
|
|
21
|
+
* IDs the session files and registers under `.planning/discovery/` mint
|
|
22
|
+
* in their first table column;
|
|
23
|
+
* - the requirement counts line equals the roll-up, and every `Dropped`
|
|
24
|
+
* requirement is listed under § Conditions;
|
|
25
|
+
* - the Go / No-Go table and the frontmatter verdict equal Session 13's
|
|
26
|
+
* assessment as recorded — the charter copies it, never re-judges it;
|
|
27
|
+
* - every Session 13 open item marked "must resolve before build" is closed
|
|
28
|
+
* or listed under § Conditions;
|
|
29
|
+
* - `DEV-` IDs resolve only against `_deviations.md`, and every row of that
|
|
30
|
+
* register appears in § Deviations (improvement plan, Batch 3);
|
|
31
|
+
* - the sign-off rows agree with `status`, and the version history ends at
|
|
32
|
+
* the frontmatter version.
|
|
33
|
+
*
|
|
34
|
+
* Reporting posture (D-43/D-53): the check fails only on structural faults.
|
|
35
|
+
* At Session 13 it reports; at `/pp-discovery-to-build` the same check runs with
|
|
36
|
+
* `--require-signed`, and a charter whose status is not `Signed` or `Signed with
|
|
37
|
+
* conditions` exits non-zero on its own identity so the handoff can hard-block
|
|
38
|
+
* on it and `--unsigned` can record the gap instead.
|
|
39
|
+
*
|
|
40
|
+
* Content-free contract (the T-02-05 precedent): output names the file, line
|
|
41
|
+
* numbers, validated IDs, section keys, field names and reason codes — never
|
|
42
|
+
* charter prose.
|
|
43
|
+
*
|
|
44
|
+
* Traversal: the check takes no positional path. The charter and every source
|
|
45
|
+
* resolve from fixed convention paths under the working directory (the T-02-08
|
|
46
|
+
* precedent).
|
|
47
|
+
*/
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
52
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
54
|
+
const io = require("./io.cjs");
|
|
55
|
+
const { output, error, ERROR_REASON } = io;
|
|
56
|
+
// `_requirements.md` is owned by `discovery rollup`; this gate consumes its
|
|
57
|
+
// parser and re-implements none of its grammar.
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
59
|
+
const rollup = require("./discovery-rollup.cjs");
|
|
60
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
61
|
+
const CHARTER_FILE = 'docs/discovery/PROJECT-CHARTER.md';
|
|
62
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
63
|
+
const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
|
|
64
|
+
const SESSION_13_FILE = `${DISCOVERY_DIR}/session-13.md`;
|
|
65
|
+
/** The twenty sections, in template order. Keys are the heading's `<!-- charter:<key> -->`. */
|
|
66
|
+
const SECTION_KEYS = Object.freeze([
|
|
67
|
+
'purpose',
|
|
68
|
+
'objectives',
|
|
69
|
+
'scope',
|
|
70
|
+
'stakeholders',
|
|
71
|
+
'approvals',
|
|
72
|
+
'solution',
|
|
73
|
+
'deviations',
|
|
74
|
+
'data',
|
|
75
|
+
'quality',
|
|
76
|
+
'validated',
|
|
77
|
+
'delivery',
|
|
78
|
+
'team',
|
|
79
|
+
'budget',
|
|
80
|
+
'risks',
|
|
81
|
+
'open-items',
|
|
82
|
+
'conditions',
|
|
83
|
+
'go-no-go',
|
|
84
|
+
'decisions',
|
|
85
|
+
'sign-off',
|
|
86
|
+
'history',
|
|
87
|
+
]);
|
|
88
|
+
/** Keys the parser reads a table from; a section without one is a structural fault. */
|
|
89
|
+
const TABLE_REQUIRED = Object.freeze(['deviations', 'open-items', 'conditions', 'go-no-go', 'sign-off', 'history']);
|
|
90
|
+
/** The one non-section key: marks the requirement-counts line in § Scope. */
|
|
91
|
+
const COUNTS_KEY = 'counts';
|
|
92
|
+
const DOCUMENT_KIND = 'project-charter';
|
|
93
|
+
const REQUIRED_FIELDS = Object.freeze([
|
|
94
|
+
'document', 'project', 'client', 'version', 'status', 'date', 'language',
|
|
95
|
+
'discovery_sessions_completed', 'go_no_go',
|
|
96
|
+
]);
|
|
97
|
+
const STATUS_VALUES = Object.freeze(['Draft', 'For sign-off', 'Signed', 'Signed with conditions', 'Rejected']);
|
|
98
|
+
const SIGNED_VALUES = Object.freeze(['Signed', 'Signed with conditions']);
|
|
99
|
+
const GO_NO_GO_VALUES = Object.freeze(['Pending', 'Go', 'Go with conditions', 'No-Go']);
|
|
100
|
+
const SIGNOFF_DECISIONS = Object.freeze(['Approve', 'Approve with conditions', 'Reject']);
|
|
101
|
+
const VERSION_RE = /^\d+\.\d+$/;
|
|
102
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
103
|
+
/** BCP-47-shaped: `nl`, `en`, `en-GB`, `nl-BE`. */
|
|
104
|
+
const LANGUAGE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
105
|
+
const KEY_RE = /<!--\s*charter:([a-z-]+)\s*-->/;
|
|
106
|
+
const HEADING_RE = /^##\s/;
|
|
107
|
+
const TABLE_SEPARATOR_RE = /^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
108
|
+
/**
|
|
109
|
+
* Session-table and register families whose IDs the charter may cite, all
|
|
110
|
+
* resolved against the first table column of the files under
|
|
111
|
+
* `.planning/discovery/`. Decisions and actions carry a session component.
|
|
112
|
+
*/
|
|
113
|
+
const TWO_PART_FAMILIES = Object.freeze(['D', 'A']);
|
|
114
|
+
const ONE_PART_FAMILIES = Object.freeze(['OI', 'R', 'CON', 'SCR', 'E', 'US', 'BO', 'ADR', 'IF', 'CR', 'RS', 'DG', 'DEV']);
|
|
115
|
+
/** The one family with a single owning register: a `DEV-` ID is minted by `_deviations.md` alone (Batch 3). */
|
|
116
|
+
const DEVIATION_FAMILY = 'DEV';
|
|
117
|
+
const DEVIATIONS_FILE = `${DISCOVERY_DIR}/_deviations.md`;
|
|
118
|
+
const ID_TOKEN_RE = /(?<![A-Za-z0-9_`-])([A-Z][A-Z0-9]*)-((?:P?\d+)(?:-\d+)?)(?![A-Za-z0-9_-])/g;
|
|
119
|
+
const FIRST_CELL_ID_RE = /^\|\s*\**\s*([A-Z][A-Z0-9]*-(?:P?\d+)(?:-\d+)?)\s*\**\s*\|/;
|
|
120
|
+
const SESSION_13_GO_NO_GO = 'Go / No-Go Assessment';
|
|
121
|
+
const SESSION_13_OPEN_ITEMS = 'Open Items Register';
|
|
122
|
+
/** Session 13's eight Go / No-Go rows: seven criteria, then the overall recommendation. */
|
|
123
|
+
const GO_NO_GO_ROWS = 8;
|
|
124
|
+
const MUST_RESOLVE_RE = /^yes\b/i;
|
|
125
|
+
const CLOSED_RE = /^(closed|resolved|done)\b/i;
|
|
126
|
+
const USAGE = 'Usage: pp-tools discovery charter-check [--require-signed]';
|
|
127
|
+
// ─── Parsing ──────────────────────────────────────────────────────────────────
|
|
128
|
+
function splitCells(line) {
|
|
129
|
+
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
130
|
+
return trimmed.split('|').map((c) => c.trim());
|
|
131
|
+
}
|
|
132
|
+
function stripEmphasis(cell) {
|
|
133
|
+
return cell.replace(/\*\*/g, '').replace(/^_|_$/g, '').trim();
|
|
134
|
+
}
|
|
135
|
+
function parseFrontmatter(lines) {
|
|
136
|
+
if (lines.length === 0 || lines[0].trim() !== '---')
|
|
137
|
+
return null;
|
|
138
|
+
const fields = new Map();
|
|
139
|
+
const fieldLines = new Map();
|
|
140
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
141
|
+
if (lines[i].trim() === '---')
|
|
142
|
+
return { fields, lines: fieldLines, endLine: i + 1 };
|
|
143
|
+
const m = /^([a-z_]+):\s*(.*)$/.exec(lines[i]);
|
|
144
|
+
if (m) {
|
|
145
|
+
fields.set(m[1], m[2].trim().replace(/^"(.*)"$/, '$1'));
|
|
146
|
+
fieldLines.set(m[1], i + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Every pipe table in a run of lines: the header is the row before the first
|
|
153
|
+
* separator, data rows follow until the first non-pipe line. A second table
|
|
154
|
+
* starts at the next separator.
|
|
155
|
+
*/
|
|
156
|
+
function parseTables(body) {
|
|
157
|
+
const tables = [];
|
|
158
|
+
for (let i = 1; i < body.length; i += 1) {
|
|
159
|
+
if (!TABLE_SEPARATOR_RE.test(body[i].text) || !body[i - 1].text.trim().startsWith('|'))
|
|
160
|
+
continue;
|
|
161
|
+
const table = { line: body[i - 1].n, header: splitCells(body[i - 1].text), rows: [] };
|
|
162
|
+
let j = i + 1;
|
|
163
|
+
for (; j < body.length; j += 1) {
|
|
164
|
+
const text = body[j].text.trim();
|
|
165
|
+
if (!text.startsWith('|'))
|
|
166
|
+
break;
|
|
167
|
+
if (TABLE_SEPARATOR_RE.test(text))
|
|
168
|
+
break;
|
|
169
|
+
table.rows.push({ line: body[j].n, cells: splitCells(text) });
|
|
170
|
+
}
|
|
171
|
+
tables.push(table);
|
|
172
|
+
i = j - 1;
|
|
173
|
+
}
|
|
174
|
+
return tables;
|
|
175
|
+
}
|
|
176
|
+
/** Sections by heading key, in file order, with their bodies and tables. */
|
|
177
|
+
function parseSections(lines, from) {
|
|
178
|
+
const sections = [];
|
|
179
|
+
const unknown = [];
|
|
180
|
+
let counts = null;
|
|
181
|
+
let current = null;
|
|
182
|
+
const bodies = [];
|
|
183
|
+
for (let i = from; i < lines.length; i += 1) {
|
|
184
|
+
const text = lines[i];
|
|
185
|
+
const n = i + 1;
|
|
186
|
+
const key = KEY_RE.exec(text);
|
|
187
|
+
if (key && key[1] === COUNTS_KEY) {
|
|
188
|
+
counts = { n, text };
|
|
189
|
+
}
|
|
190
|
+
else if (key && !HEADING_RE.test(text)) {
|
|
191
|
+
unknown.push({ n, key: key[1] });
|
|
192
|
+
}
|
|
193
|
+
if (HEADING_RE.test(text)) {
|
|
194
|
+
if (current)
|
|
195
|
+
bodies.push(current.body);
|
|
196
|
+
if (key && key[1] !== COUNTS_KEY) {
|
|
197
|
+
current = { key: key[1], line: n, tables: [], body: [] };
|
|
198
|
+
sections.push(current);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
current = null;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (current)
|
|
206
|
+
current.body.push({ n, text });
|
|
207
|
+
}
|
|
208
|
+
for (const s of sections)
|
|
209
|
+
s.tables = parseTables(s.body);
|
|
210
|
+
return { sections, counts, unknown };
|
|
211
|
+
}
|
|
212
|
+
function readIfPresent(abs) {
|
|
213
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
214
|
+
return { kind: 'absent' };
|
|
215
|
+
try {
|
|
216
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return { kind: 'unreadable' };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Every ID minted in the first column of any table under `.planning/discovery/*.md`,
|
|
224
|
+
* plus the `DEV-` IDs `_deviations.md` mints on its own (`deviations` is null when
|
|
225
|
+
* that register does not exist).
|
|
226
|
+
*/
|
|
227
|
+
function collectMintedIds(cwd) {
|
|
228
|
+
const ids = new Set();
|
|
229
|
+
let deviations = null;
|
|
230
|
+
const unreadable = [];
|
|
231
|
+
const dir = node_path_1.default.join(cwd, DISCOVERY_DIR);
|
|
232
|
+
let names = [];
|
|
233
|
+
try {
|
|
234
|
+
names = node_fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.md') && f !== '_requirements.md').sort();
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return { ids, deviations, unreadable };
|
|
238
|
+
}
|
|
239
|
+
for (const name of names) {
|
|
240
|
+
const read = readIfPresent(node_path_1.default.join(dir, name));
|
|
241
|
+
if (read.kind === 'absent')
|
|
242
|
+
continue;
|
|
243
|
+
if (read.kind === 'unreadable') {
|
|
244
|
+
unreadable.push(`${DISCOVERY_DIR}/${name}`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const isDeviations = `${DISCOVERY_DIR}/${name}` === DEVIATIONS_FILE;
|
|
248
|
+
if (isDeviations)
|
|
249
|
+
deviations = new Set();
|
|
250
|
+
for (const line of read.content.split(/\r?\n/)) {
|
|
251
|
+
const m = FIRST_CELL_ID_RE.exec(line);
|
|
252
|
+
if (!m)
|
|
253
|
+
continue;
|
|
254
|
+
if (m[1].startsWith(`${DEVIATION_FAMILY}-`)) {
|
|
255
|
+
if (isDeviations)
|
|
256
|
+
deviations.add(m[1]);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
ids.add(m[1]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { ids, deviations, unreadable };
|
|
264
|
+
}
|
|
265
|
+
/** `_requirements.md` through its owner's parser: absent → null, unparseable → 'unparseable'. */
|
|
266
|
+
function readRequirements(cwd) {
|
|
267
|
+
const read = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
|
|
268
|
+
if (read.kind === 'absent')
|
|
269
|
+
return null;
|
|
270
|
+
if (read.kind === 'unreadable')
|
|
271
|
+
return 'unreadable';
|
|
272
|
+
const parsed = rollup.parseRequirementsArtifact(read.content);
|
|
273
|
+
if (!parsed.ok)
|
|
274
|
+
return 'unparseable';
|
|
275
|
+
const prefixes = new Set(parsed.registry.map((r) => r.prefix));
|
|
276
|
+
const ids = new Set();
|
|
277
|
+
const liveByPrefix = new Map();
|
|
278
|
+
for (const p of prefixes)
|
|
279
|
+
liveByPrefix.set(p, 0);
|
|
280
|
+
const dropped = [];
|
|
281
|
+
for (const row of parsed.rows) {
|
|
282
|
+
ids.add(row.id);
|
|
283
|
+
if (row.disposition === 'Dropped')
|
|
284
|
+
dropped.push(row.id);
|
|
285
|
+
else
|
|
286
|
+
liveByPrefix.set(row.prefix, (liveByPrefix.get(row.prefix) ?? 0) + 1);
|
|
287
|
+
}
|
|
288
|
+
return { prefixes, ids, liveByPrefix, dropped };
|
|
289
|
+
}
|
|
290
|
+
/** A Notes Template table in a session file: the first table after the named label (bold or heading). */
|
|
291
|
+
function sessionTable(lines, label) {
|
|
292
|
+
const norm = (s) => s.replace(/^#+\s*/, '').replace(/\*\*/g, '').trim();
|
|
293
|
+
const at = lines.findIndex((l) => norm(l) === label);
|
|
294
|
+
if (at === -1)
|
|
295
|
+
return null;
|
|
296
|
+
const body = [];
|
|
297
|
+
for (let i = at + 1; i < lines.length; i += 1) {
|
|
298
|
+
if (/^#{1,3}\s/.test(lines[i]) || /^\*\*[^*]+\*\*$/.test(lines[i].trim()))
|
|
299
|
+
break;
|
|
300
|
+
body.push({ n: i + 1, text: lines[i] });
|
|
301
|
+
}
|
|
302
|
+
const tables = parseTables(body);
|
|
303
|
+
return tables.length > 0 ? tables[0] : null;
|
|
304
|
+
}
|
|
305
|
+
function readSession13(cwd) {
|
|
306
|
+
const read = readIfPresent(node_path_1.default.join(cwd, SESSION_13_FILE));
|
|
307
|
+
if (read.kind === 'absent')
|
|
308
|
+
return null;
|
|
309
|
+
if (read.kind === 'unreadable')
|
|
310
|
+
return 'unreadable';
|
|
311
|
+
const lines = read.content.split(/\r?\n/);
|
|
312
|
+
const go = sessionTable(lines, SESSION_13_GO_NO_GO);
|
|
313
|
+
const open = sessionTable(lines, SESSION_13_OPEN_ITEMS);
|
|
314
|
+
return {
|
|
315
|
+
goNoGo: go ? go.rows.map((r) => ({ status: stripEmphasis(r.cells[1] ?? '') })) : null,
|
|
316
|
+
openItems: open
|
|
317
|
+
? open.rows
|
|
318
|
+
.filter((r) => /^OI-\d+$/.test(stripEmphasis(r.cells[0] ?? '')))
|
|
319
|
+
.map((r) => ({
|
|
320
|
+
id: stripEmphasis(r.cells[0] ?? ''),
|
|
321
|
+
mustResolve: MUST_RESOLVE_RE.test(stripEmphasis(r.cells[4] ?? '')),
|
|
322
|
+
closed: CLOSED_RE.test(stripEmphasis(r.cells[7] ?? '')),
|
|
323
|
+
}))
|
|
324
|
+
: null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** Check one charter text against its sources. Pure over its inputs. */
|
|
328
|
+
function checkCharter(content, sources, requireSigned = false) {
|
|
329
|
+
const violations = [];
|
|
330
|
+
const lines = content.split(/\r?\n/);
|
|
331
|
+
// 1 ─ Frontmatter
|
|
332
|
+
const fm = parseFrontmatter(lines);
|
|
333
|
+
let status = null;
|
|
334
|
+
let goNoGo = null;
|
|
335
|
+
let version = null;
|
|
336
|
+
let language = null;
|
|
337
|
+
if (fm === null) {
|
|
338
|
+
violations.push({ reason: 'frontmatter_missing', line: 1 });
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
for (const f of REQUIRED_FIELDS) {
|
|
342
|
+
if (!fm.fields.has(f) || fm.fields.get(f) === '')
|
|
343
|
+
violations.push({ reason: 'field_missing', id: f });
|
|
344
|
+
}
|
|
345
|
+
const at = (f) => fm.lines.get(f);
|
|
346
|
+
const v = (f) => fm.fields.get(f) ?? '';
|
|
347
|
+
if (fm.fields.has('document') && v('document') !== DOCUMENT_KIND)
|
|
348
|
+
violations.push({ reason: 'bad_document', line: at('document') });
|
|
349
|
+
if (fm.fields.has('status')) {
|
|
350
|
+
if (STATUS_VALUES.includes(v('status')))
|
|
351
|
+
status = v('status');
|
|
352
|
+
else
|
|
353
|
+
violations.push({ reason: 'bad_status', line: at('status') });
|
|
354
|
+
}
|
|
355
|
+
if (fm.fields.has('go_no_go')) {
|
|
356
|
+
if (GO_NO_GO_VALUES.includes(v('go_no_go')))
|
|
357
|
+
goNoGo = v('go_no_go');
|
|
358
|
+
else
|
|
359
|
+
violations.push({ reason: 'bad_go_no_go', line: at('go_no_go') });
|
|
360
|
+
}
|
|
361
|
+
if (fm.fields.has('version')) {
|
|
362
|
+
if (VERSION_RE.test(v('version')))
|
|
363
|
+
version = v('version');
|
|
364
|
+
else
|
|
365
|
+
violations.push({ reason: 'bad_version', line: at('version') });
|
|
366
|
+
}
|
|
367
|
+
if (fm.fields.has('date') && !DATE_RE.test(v('date')))
|
|
368
|
+
violations.push({ reason: 'bad_date', line: at('date') });
|
|
369
|
+
if (fm.fields.has('language')) {
|
|
370
|
+
if (LANGUAGE_RE.test(v('language')))
|
|
371
|
+
language = v('language');
|
|
372
|
+
else
|
|
373
|
+
violations.push({ reason: 'bad_language', line: at('language') });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 2 ─ Sections: present once each, in order, nothing unknown
|
|
377
|
+
const { sections, counts, unknown } = parseSections(lines, fm ? fm.endLine : 0);
|
|
378
|
+
for (const u of unknown)
|
|
379
|
+
violations.push({ reason: 'section_unknown', line: u.n, id: u.key });
|
|
380
|
+
const seen = new Map();
|
|
381
|
+
for (const s of sections) {
|
|
382
|
+
if (!SECTION_KEYS.includes(s.key)) {
|
|
383
|
+
violations.push({ reason: 'section_unknown', line: s.line, id: s.key });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (seen.has(s.key))
|
|
387
|
+
violations.push({ reason: 'section_duplicate', line: s.line, id: s.key });
|
|
388
|
+
else
|
|
389
|
+
seen.set(s.key, s);
|
|
390
|
+
}
|
|
391
|
+
for (const k of SECTION_KEYS) {
|
|
392
|
+
if (!seen.has(k))
|
|
393
|
+
violations.push({ reason: 'section_missing', id: k });
|
|
394
|
+
}
|
|
395
|
+
const order = sections.filter((s) => seen.get(s.key) === s).map((s) => s.key);
|
|
396
|
+
const expected = SECTION_KEYS.filter((k) => seen.has(k));
|
|
397
|
+
for (let i = 0; i < order.length; i += 1) {
|
|
398
|
+
if (order[i] !== expected[i]) {
|
|
399
|
+
violations.push({ reason: 'section_order', line: seen.get(order[i])?.line, id: order[i] });
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const k of TABLE_REQUIRED) {
|
|
404
|
+
const s = seen.get(k);
|
|
405
|
+
if (s && s.tables.length === 0)
|
|
406
|
+
violations.push({ reason: 'table_missing', line: s.line, id: k });
|
|
407
|
+
}
|
|
408
|
+
// 3 ─ Every ID a table cell cites resolves
|
|
409
|
+
let idsChecked = 0;
|
|
410
|
+
const requirements = sources.requirements;
|
|
411
|
+
for (const s of seen.values()) {
|
|
412
|
+
if (s.key === 'history')
|
|
413
|
+
continue;
|
|
414
|
+
for (const t of s.tables) {
|
|
415
|
+
for (const row of t.rows) {
|
|
416
|
+
for (const cell of row.cells) {
|
|
417
|
+
for (const m of cell.matchAll(ID_TOKEN_RE)) {
|
|
418
|
+
const prefix = m[1];
|
|
419
|
+
const token = `${prefix}-${m[2]}`;
|
|
420
|
+
const twoPart = /^P?\d+-\d+$/.test(m[2]);
|
|
421
|
+
if (TWO_PART_FAMILIES.includes(prefix)) {
|
|
422
|
+
idsChecked += 1;
|
|
423
|
+
if (!/^(?:\d{2}|P\d{2})-\d+$/.test(m[2]) || !sources.minted.has(token))
|
|
424
|
+
violations.push({ reason: 'unknown_id', line: row.line, id: token });
|
|
425
|
+
}
|
|
426
|
+
else if (prefix === DEVIATION_FAMILY) {
|
|
427
|
+
idsChecked += 1;
|
|
428
|
+
if (twoPart || sources.deviations === null || !sources.deviations.has(token))
|
|
429
|
+
violations.push({ reason: 'unknown_id', line: row.line, id: token });
|
|
430
|
+
}
|
|
431
|
+
else if (ONE_PART_FAMILIES.includes(prefix)) {
|
|
432
|
+
idsChecked += 1;
|
|
433
|
+
if (twoPart || !sources.minted.has(token))
|
|
434
|
+
violations.push({ reason: 'unknown_id', line: row.line, id: token });
|
|
435
|
+
}
|
|
436
|
+
else if (requirements && requirements !== 'unparseable' && requirements.prefixes.has(`${prefix}-`)) {
|
|
437
|
+
idsChecked += 1;
|
|
438
|
+
if (twoPart || !requirements.ids.has(token))
|
|
439
|
+
violations.push({ reason: 'unknown_id', line: row.line, id: token });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// 3b ─ Every deviation on file is listed in § Deviations (Batch 3)
|
|
447
|
+
const deviationsSection = seen.get('deviations');
|
|
448
|
+
if (sources.deviations !== null && deviationsSection) {
|
|
449
|
+
const listed = new Set();
|
|
450
|
+
for (const t of deviationsSection.tables)
|
|
451
|
+
for (const row of t.rows)
|
|
452
|
+
for (const m of (row.cells[0] ?? '').matchAll(ID_TOKEN_RE))
|
|
453
|
+
listed.add(`${m[1]}-${m[2]}`);
|
|
454
|
+
for (const id of sources.deviations)
|
|
455
|
+
if (!listed.has(id))
|
|
456
|
+
violations.push({ reason: 'deviation_missing', line: deviationsSection.line, id });
|
|
457
|
+
}
|
|
458
|
+
// 4 ─ Requirement counts and dropped rows, when the register is on file
|
|
459
|
+
const conditions = seen.get('conditions');
|
|
460
|
+
const conditionsText = conditions ? conditions.tables.flatMap((t) => t.rows.flatMap((r) => r.cells)).join(' ') : '';
|
|
461
|
+
if (requirements === 'unparseable') {
|
|
462
|
+
violations.push({ reason: 'requirements_unparseable' });
|
|
463
|
+
}
|
|
464
|
+
else if (requirements) {
|
|
465
|
+
if (!counts) {
|
|
466
|
+
violations.push({ reason: 'counts_missing', line: seen.get('scope')?.line });
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const found = new Map();
|
|
470
|
+
for (const m of counts.text.replace(KEY_RE, '').matchAll(/([A-Z][A-Z0-9]*-)\s+(\d+)/g))
|
|
471
|
+
found.set(m[1], Number(m[2]));
|
|
472
|
+
const droppedM = /Dropped\s+(\d+)/.exec(counts.text);
|
|
473
|
+
let mismatch = droppedM === null || Number(droppedM[1]) !== requirements.dropped.length;
|
|
474
|
+
if (found.size !== requirements.liveByPrefix.size)
|
|
475
|
+
mismatch = true;
|
|
476
|
+
for (const [p, n] of requirements.liveByPrefix)
|
|
477
|
+
if (found.get(p) !== n)
|
|
478
|
+
mismatch = true;
|
|
479
|
+
if (mismatch)
|
|
480
|
+
violations.push({ reason: 'counts_mismatch', line: counts.n });
|
|
481
|
+
}
|
|
482
|
+
if (conditions) {
|
|
483
|
+
for (const id of requirements.dropped) {
|
|
484
|
+
if (!new RegExp(`(?<![A-Z0-9-])${id}(?![0-9-])`).test(conditionsText))
|
|
485
|
+
violations.push({ reason: 'dropped_missing', id });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 5 ─ Go / No-Go: recorded, not re-judged
|
|
490
|
+
if (goNoGo === 'Pending' && status !== null && status !== 'Draft')
|
|
491
|
+
violations.push({ reason: 'go_no_go_pending', line: fm?.lines.get('go_no_go') });
|
|
492
|
+
const go = seen.get('go-no-go');
|
|
493
|
+
const goTable = go && go.tables.length > 0 ? go.tables[0] : null;
|
|
494
|
+
if (goTable && goTable.rows.length !== GO_NO_GO_ROWS)
|
|
495
|
+
violations.push({ reason: 'go_no_go_mismatch', line: goTable.line, id: 'rows' });
|
|
496
|
+
if (goTable && goTable.rows.length === GO_NO_GO_ROWS && goNoGo !== null && goNoGo !== 'Pending') {
|
|
497
|
+
const overall = stripEmphasis(goTable.rows[GO_NO_GO_ROWS - 1].cells[1] ?? '');
|
|
498
|
+
if (overall !== goNoGo)
|
|
499
|
+
violations.push({ reason: 'go_no_go_mismatch', line: goTable.rows[GO_NO_GO_ROWS - 1].line, id: 'overall' });
|
|
500
|
+
}
|
|
501
|
+
const s13 = sources.session13;
|
|
502
|
+
if (s13 && s13.goNoGo && s13.goNoGo.length === GO_NO_GO_ROWS && goTable && goTable.rows.length === GO_NO_GO_ROWS) {
|
|
503
|
+
const recordedOverall = s13.goNoGo[GO_NO_GO_ROWS - 1].status;
|
|
504
|
+
const recorded = recordedOverall !== '';
|
|
505
|
+
if (recorded) {
|
|
506
|
+
for (let i = 0; i < GO_NO_GO_ROWS; i += 1) {
|
|
507
|
+
if (stripEmphasis(goTable.rows[i].cells[1] ?? '') !== s13.goNoGo[i].status) {
|
|
508
|
+
violations.push({ reason: 'go_no_go_mismatch', line: goTable.rows[i].line, id: `row-${i + 1}` });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (goNoGo !== null && goNoGo !== recordedOverall)
|
|
512
|
+
violations.push({ reason: 'go_no_go_mismatch', line: fm?.lines.get('go_no_go'), id: 'frontmatter' });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// 6 ─ Must-resolve open items are closed or conditions
|
|
516
|
+
if (s13 && s13.openItems && conditions) {
|
|
517
|
+
for (const item of s13.openItems) {
|
|
518
|
+
if (item.mustResolve && !item.closed && !new RegExp(`(?<![A-Z0-9-])${item.id}(?![0-9-])`).test(conditionsText)) {
|
|
519
|
+
violations.push({ reason: 'must_resolve_open', id: item.id });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// 7 ─ Sign-off rows agree with status
|
|
524
|
+
const signoff = seen.get('sign-off');
|
|
525
|
+
const signTable = signoff && signoff.tables.length > 0 ? signoff.tables[0] : null;
|
|
526
|
+
if (signTable) {
|
|
527
|
+
const decisions = [];
|
|
528
|
+
for (const row of signTable.rows) {
|
|
529
|
+
const d = stripEmphasis(row.cells[2] ?? '');
|
|
530
|
+
if (d !== '' && !SIGNOFF_DECISIONS.includes(d))
|
|
531
|
+
violations.push({ reason: 'bad_signoff_decision', line: row.line });
|
|
532
|
+
else
|
|
533
|
+
decisions.push(d);
|
|
534
|
+
}
|
|
535
|
+
const all = (pred) => decisions.length > 0 && decisions.every(pred);
|
|
536
|
+
let consistent = true;
|
|
537
|
+
if (status === 'Signed')
|
|
538
|
+
consistent = all((d) => d === 'Approve');
|
|
539
|
+
else if (status === 'Signed with conditions')
|
|
540
|
+
consistent = all((d) => d === 'Approve' || d === 'Approve with conditions') && decisions.includes('Approve with conditions');
|
|
541
|
+
else if (status === 'Rejected')
|
|
542
|
+
consistent = decisions.includes('Reject');
|
|
543
|
+
if (!consistent)
|
|
544
|
+
violations.push({ reason: 'signoff_status_mismatch', line: signTable.line });
|
|
545
|
+
}
|
|
546
|
+
// 8 ─ Version history ends at the frontmatter version
|
|
547
|
+
const history = seen.get('history');
|
|
548
|
+
const histTable = history && history.tables.length > 0 ? history.tables[0] : null;
|
|
549
|
+
if (histTable) {
|
|
550
|
+
if (histTable.rows.length === 0)
|
|
551
|
+
violations.push({ reason: 'history_missing', line: histTable.line });
|
|
552
|
+
else {
|
|
553
|
+
for (const row of histTable.rows) {
|
|
554
|
+
if (!VERSION_RE.test(stripEmphasis(row.cells[0] ?? '')))
|
|
555
|
+
violations.push({ reason: 'bad_version', line: row.line });
|
|
556
|
+
}
|
|
557
|
+
const last = stripEmphasis(histTable.rows[histTable.rows.length - 1].cells[0] ?? '');
|
|
558
|
+
if (version !== null && VERSION_RE.test(last) && last !== version)
|
|
559
|
+
violations.push({ reason: 'history_version_mismatch', line: histTable.rows[histTable.rows.length - 1].line });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// 9 ─ The handoff arm
|
|
563
|
+
if (requireSigned && (status === null || !SIGNED_VALUES.includes(status))) {
|
|
564
|
+
violations.push({ reason: 'charter_unsigned', line: fm?.lines.get('status') });
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
ok: violations.length === 0,
|
|
568
|
+
status,
|
|
569
|
+
go_no_go: goNoGo,
|
|
570
|
+
version,
|
|
571
|
+
language,
|
|
572
|
+
sections: seen.size,
|
|
573
|
+
ids_checked: idsChecked,
|
|
574
|
+
violations,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/** Check the charter of the project at `cwd`. */
|
|
578
|
+
function checkProjectCharter(cwd, requireSigned = false) {
|
|
579
|
+
const empty = { status: null, go_no_go: null, version: null, language: null, sections: 0, ids_checked: 0 };
|
|
580
|
+
const read = readIfPresent(node_path_1.default.join(cwd, CHARTER_FILE));
|
|
581
|
+
if (read.kind === 'absent')
|
|
582
|
+
return { ok: false, file: CHARTER_FILE, ...empty, violations: [{ reason: 'charter_missing' }] };
|
|
583
|
+
if (read.kind === 'unreadable')
|
|
584
|
+
return { ok: false, file: CHARTER_FILE, ...empty, violations: [{ reason: 'artifact_unreadable', id: CHARTER_FILE }] };
|
|
585
|
+
const content = read.content;
|
|
586
|
+
const minted = collectMintedIds(cwd);
|
|
587
|
+
const requirements = readRequirements(cwd);
|
|
588
|
+
const session13 = readSession13(cwd);
|
|
589
|
+
const sources = {
|
|
590
|
+
minted: minted.ids,
|
|
591
|
+
deviations: minted.deviations,
|
|
592
|
+
requirements: requirements === 'unreadable' ? null : requirements,
|
|
593
|
+
session13: session13 === 'unreadable' ? null : session13,
|
|
594
|
+
};
|
|
595
|
+
const result = checkCharter(content, sources, requireSigned);
|
|
596
|
+
for (const p of minted.unreadable)
|
|
597
|
+
result.violations.push({ reason: 'artifact_unreadable', id: p });
|
|
598
|
+
if (requirements === 'unreadable')
|
|
599
|
+
result.violations.push({ reason: 'artifact_unreadable', id: REQUIREMENTS_FILE });
|
|
600
|
+
if (session13 === 'unreadable')
|
|
601
|
+
result.violations.push({ reason: 'artifact_unreadable', id: SESSION_13_FILE });
|
|
602
|
+
result.ok = result.violations.length === 0;
|
|
603
|
+
return { file: CHARTER_FILE, ...result };
|
|
604
|
+
}
|
|
605
|
+
function formatViolation(v) {
|
|
606
|
+
const where = v.line !== undefined ? `line ${v.line}: ` : '';
|
|
607
|
+
const what = v.id !== undefined ? ` (${v.id.slice(0, 80)})` : '';
|
|
608
|
+
return ` - ${where}${v.reason}${what}`;
|
|
609
|
+
}
|
|
610
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
611
|
+
/**
|
|
612
|
+
* CLI adapter for `pp-tools discovery charter-check [--require-signed]`
|
|
613
|
+
* (dotted form `pp-tools query discovery.charter-check`).
|
|
614
|
+
*
|
|
615
|
+
* Exit semantics: a structural fault exits non-zero via
|
|
616
|
+
* `ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED`; a structurally sound charter
|
|
617
|
+
* that `--require-signed` finds unsigned exits via
|
|
618
|
+
* `ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED`. The payload is emitted on stdout
|
|
619
|
+
* first on both failure paths, so the status survives alongside the error.
|
|
620
|
+
*/
|
|
621
|
+
function cmdDiscoveryCharterCheck(cwd, args, raw) {
|
|
622
|
+
// args arrive shaped ['discovery', 'charter-check', ...flags].
|
|
623
|
+
const rest = args.slice(2);
|
|
624
|
+
const requireSigned = rest.includes('--require-signed');
|
|
625
|
+
if (rest.some((a) => a !== '--require-signed')) {
|
|
626
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
627
|
+
return; // unreachable — error() exits
|
|
628
|
+
}
|
|
629
|
+
const result = checkProjectCharter(cwd, requireSigned);
|
|
630
|
+
const payload = {
|
|
631
|
+
ok: result.ok,
|
|
632
|
+
file: result.file,
|
|
633
|
+
status: result.status,
|
|
634
|
+
go_no_go: result.go_no_go,
|
|
635
|
+
version: result.version,
|
|
636
|
+
language: result.language,
|
|
637
|
+
sections: result.sections,
|
|
638
|
+
ids_checked: result.ids_checked,
|
|
639
|
+
};
|
|
640
|
+
if (!result.ok) {
|
|
641
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
642
|
+
const onlyUnsigned = result.violations.every((v) => v.reason === 'charter_unsigned');
|
|
643
|
+
error(`discovery charter-check: ${result.violations.length} violation(s) in ${result.file}:\n` +
|
|
644
|
+
result.violations.map(formatViolation).join('\n'), onlyUnsigned ? ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED : ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED);
|
|
645
|
+
return; // unreachable — error() exits
|
|
646
|
+
}
|
|
647
|
+
output(payload, raw);
|
|
648
|
+
}
|
|
649
|
+
module.exports = {
|
|
650
|
+
CHARTER_FILE,
|
|
651
|
+
SECTION_KEYS,
|
|
652
|
+
TABLE_REQUIRED,
|
|
653
|
+
COUNTS_KEY,
|
|
654
|
+
STATUS_VALUES,
|
|
655
|
+
SIGNED_VALUES,
|
|
656
|
+
GO_NO_GO_VALUES,
|
|
657
|
+
SIGNOFF_DECISIONS,
|
|
658
|
+
CHARTER_CHECK_FAILED: ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED,
|
|
659
|
+
CHARTER_UNSIGNED: ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED,
|
|
660
|
+
parseTables,
|
|
661
|
+
checkCharter,
|
|
662
|
+
checkProjectCharter,
|
|
663
|
+
cmdDiscoveryCharterCheck,
|
|
664
|
+
};
|