@nodii/dev 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -5
- package/dist/api.js +2 -0
- package/dist/cli.js +20 -5
- package/dist/core.js +1 -1
- package/dist/devops-openapi.json +1 -1
- package/dist/docs.js +53 -4
- package/dist/figma.js +92 -1
- package/dist/hooks.js +3 -1
- package/dist/lanes.js +40 -2
- package/dist/upload.js +42 -1
- package/package.json +1 -1
package/dist/docs.js
CHANGED
|
@@ -93,12 +93,34 @@ function fencedLines(lines) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The document's own clause id at the start of a line in a definition
|
|
98
|
+
* position (Dev issue 80), as Dev reads it: a bullet, text inside the bold,
|
|
99
|
+
* a bold paragraph, a heading, a bold table row or a numbered item. Ranges and
|
|
100
|
+
* lists (`**A-1 to A-4:**`, `**A-1, A-2:**`) are references. Null otherwise.
|
|
101
|
+
*/
|
|
102
|
+
export function sourceIdAt(line, code) {
|
|
103
|
+
const m = /^\s*(?:([-*+])\s+|(\d+)\.\s+|(#{1,6})\s+|(\|)\s*)?(\*\*)?(.*)$/.exec(line);
|
|
104
|
+
if (!m) return null;
|
|
105
|
+
const [, bullet, numbered, heading, pipe, bold, rest = ""] = m;
|
|
106
|
+
if (!bold && !heading) return null;
|
|
107
|
+
const id = new RegExp(`^(${CLAUSE_ID})(?![\\d.]*\\d)`).exec(rest)?.[1];
|
|
108
|
+
// The document's own id: `TELE-3`, not the sibling code's `TELE-POR-3`.
|
|
109
|
+
if (!id || !new RegExp(`^${code.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-S?\\d`).test(id)) return null;
|
|
110
|
+
const after = rest.slice(id.length);
|
|
111
|
+
// A list or range (`, X-2`, ` to X-4`, `–4`) is a reference, in or just after the bold; a dash before prose is not.
|
|
112
|
+
if (new RegExp(`^(?:\\*\\*)?\\s*(?:,|to\\s|and\\s|or\\s|[\u2013\u2014-])\\s*(?:\\*\\*)?(?:${CLAUSE_ID}|\\d)`).test(after)) return null;
|
|
113
|
+
const form = bullet && bold ? (/^\*\*(?=\s|$)/.test(after) ? "bullet" : "bullet-malformed") : heading ? "heading" : pipe ? "table-row" : numbered ? "numbered" : "bold-paragraph";
|
|
114
|
+
return { id, form };
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
/** Sections, clause definitions and parked items, skipping fenced code, with 1-based lines of the whole file. */
|
|
97
|
-
export function parseBody(content, bodyStartLine = 1) {
|
|
118
|
+
export function parseBody(content, bodyStartLine = 1, code = "") {
|
|
98
119
|
const lines = content.split("\n");
|
|
99
120
|
const sections = [];
|
|
100
121
|
const clauses = [];
|
|
101
122
|
const parked = [];
|
|
123
|
+
const sourceIds = [];
|
|
102
124
|
const fenced = fencedLines(lines);
|
|
103
125
|
let section = "";
|
|
104
126
|
for (let i = bodyStartLine - 1; i < lines.length; i++) {
|
|
@@ -106,17 +128,43 @@ export function parseBody(content, bodyStartLine = 1) {
|
|
|
106
128
|
if (fenced[i]) continue;
|
|
107
129
|
const h2 = /^##\s+(.+?)\s*#*\s*$/.exec(line);
|
|
108
130
|
if (h2 && !line.startsWith("###")) {
|
|
131
|
+
const heading = code ? sourceIdAt(line, code) : null;
|
|
132
|
+
if (heading) sourceIds.push({ ...heading, line: i + 1 });
|
|
109
133
|
section = sectionTitle(h2[1]);
|
|
110
134
|
sections.push({ title: section, line: i + 1 });
|
|
111
135
|
continue;
|
|
112
136
|
}
|
|
113
137
|
// `- **TELE-32** Undecided: …` parks that clause; it is not a second definition of it.
|
|
114
138
|
const p = parkedItem(line);
|
|
115
|
-
if (p !== null) { parked.push({ text: p, line: i + 1 }); continue; }
|
|
139
|
+
if (p !== null) { parked.push({ text: p, line: i + 1, clauseId: PARKED.exec(line)?.[1] ?? null }); continue; }
|
|
140
|
+
const source = code ? sourceIdAt(line, code) : null;
|
|
141
|
+
if (source) sourceIds.push({ ...source, line: i + 1 });
|
|
116
142
|
const clause = CLAUSE_DEF.exec(line);
|
|
117
143
|
if (clause) clauses.push({ id: clause[1], section, line: i + 1 });
|
|
118
144
|
}
|
|
119
|
-
return { sections, clauses, parked };
|
|
145
|
+
return { sections, clauses, parked, sourceIds };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Dev issue 80: refuse every non-bullet clause form, then state the count invariant. */
|
|
149
|
+
export function clauseFormErrors(body) {
|
|
150
|
+
const defined = new Set(body.clauses.map((c) => c.id));
|
|
151
|
+
// A parked line names its clause; a `### X-12` group heading is defined by its sub-clauses.
|
|
152
|
+
// Only a heading groups sub-clauses: `**X-12** … MUST …` over `- **X-12.1**` is a clause of its own.
|
|
153
|
+
const known = (id, form) => defined.has(id) || body.parked.some((p) => p.clauseId === id) || (form === "heading" && body.clauses.some((c) => c.id.startsWith(`${id}.`)));
|
|
154
|
+
const missing = new Map();
|
|
155
|
+
const errors = [];
|
|
156
|
+
for (const s of body.sourceIds) {
|
|
157
|
+
if (known(s.id, s.form) || missing.has(s.id)) continue;
|
|
158
|
+
missing.set(s.id, s.line);
|
|
159
|
+
errors.push(s.form === "bullet-malformed"
|
|
160
|
+
? { code: "CLAUSE_MALFORMED", message: `clause \`${s.id}\` has text inside its bold; write \`- **${s.id}** …\` with any tag such as \`[INTENDED]\` after the bold (Dev issue 80)`, line: s.line }
|
|
161
|
+
: { code: "CLAUSE_NOT_A_BULLET", message: `clause \`${s.id}\` is written as a ${s.form.replace("-", " ")}; Dev reads a clause only as a bullet, \`- **${s.id}** …\` (Dev issue 80)`, line: s.line });
|
|
162
|
+
}
|
|
163
|
+
if (missing.size) {
|
|
164
|
+
const source = defined.size + missing.size;
|
|
165
|
+
errors.push({ code: "CLAUSE_COUNT_MISMATCH", message: `Dev would store ${defined.size} clauses but the source defines ${source}; missing: ${[...missing.keys()].join(", ")} (Dev issue 80)` });
|
|
166
|
+
}
|
|
167
|
+
return errors;
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
/**
|
|
@@ -188,9 +236,10 @@ export function validateDocument(content) {
|
|
|
188
236
|
else if (groundedAt && !GROUNDED_AT.test(groundedAt))
|
|
189
237
|
errors.push({ code: "FRONT_MATTER_FIELD_MISSING", message: `grounded_at \`${groundedAt}\` must be \`<repo>@<sha>\``, line: 1 });
|
|
190
238
|
|
|
191
|
-
const body = parseBody(text, bodyStartLine);
|
|
239
|
+
const body = parseBody(text, bodyStartLine, code);
|
|
192
240
|
if (kind) errors.push(...sectionErrors(body.sections, kind));
|
|
193
241
|
if (code) {
|
|
242
|
+
errors.push(...clauseFormErrors(body));
|
|
194
243
|
const seen = new Map();
|
|
195
244
|
for (const c of body.clauses) {
|
|
196
245
|
if (!c.id.startsWith(`${code}-`)) { errors.push({ code: "CLAUSE_CODE_MISMATCH", message: `clause \`${c.id}\` does not start with the document code \`${code}-\` (M05-6)`, line: c.line }); continue; }
|
package/dist/figma.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { CliError, devRequest, encode } from "./core.js";
|
|
2
3
|
|
|
3
4
|
/** A Figma file key, or the key inside a figma.com file link (a branch link's branch key). */
|
|
4
5
|
export function figmaFileKey(value) {
|
|
@@ -13,3 +14,93 @@ export function figmaFileKey(value) {
|
|
|
13
14
|
/** What to run when Dev has no row for a Figma file (DESIGN_FILE_NOT_FOUND). */
|
|
14
15
|
export const notRegisteredHint = (key) =>
|
|
15
16
|
`${key} is NOT registered in Dev: run nodii-dev figma file add ${key} --name NAME --kind design-system|surfaces|prototype [--module M]`;
|
|
17
|
+
|
|
18
|
+
// Dev's refusals of resubmitDesignChangeSet, said in terms of what to do next.
|
|
19
|
+
const RESUBMIT_REFUSALS = {
|
|
20
|
+
CHANGE_SET_NOT_FOUND: (id) => `No change set ${id} in Dev.`,
|
|
21
|
+
CHANGE_SET_HOLDER_REQUIRED: (id) => `You don't hold ${id}; only its holder re-submits it.`,
|
|
22
|
+
INVALID_CHANGE_SET_STATE: (id) =>
|
|
23
|
+
`${id} is not applying; re-submit once every item is decided and some were rejected (see nodii-dev figma changeset show ${id}).`,
|
|
24
|
+
CHANGE_SET_HAS_NO_REJECTIONS: (id) => `Nothing in ${id} is rejected; run nodii-dev figma apply ${id} --figma-version VERSION instead.`,
|
|
25
|
+
REJECTED_ITEMS_MISSING: (id, details, message) =>
|
|
26
|
+
Array.isArray(details?.missing)
|
|
27
|
+
? [`Re-submit every rejected item in ${id}; missing:`, ...details.missing.map((i) => ` ${i.figmaNodeId} (${i.id}): ${i.comment ?? "no comment"}`)].join("\n")
|
|
28
|
+
: message,
|
|
29
|
+
CHANGE_ITEM_NOT_REJECTED: (id, details, message) =>
|
|
30
|
+
Array.isArray(details?.rejected)
|
|
31
|
+
? [`${message}`, `Rejected items in ${id}:`, ...details.rejected.map((i) => ` ${i.figmaNodeId} (${i.id})`)].join("\n")
|
|
32
|
+
: message,
|
|
33
|
+
CHANGE_SET_CHANGED: (id) => `${id} changed while re-submitting; read it with nodii-dev figma changeset show ${id} and try again.`,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** A round-2 payload: { figmaVersion, items: [{ itemId | figmaNodeId, variants: [{ nodeId, beforePngBase64?, afterPngBase64? }] }] }. */
|
|
37
|
+
export function resubmitPayload(text, file) {
|
|
38
|
+
let body;
|
|
39
|
+
try {
|
|
40
|
+
body = JSON.parse(text);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new CliError("USAGE", `${file} is not JSON: ${error.message}`, 2);
|
|
43
|
+
}
|
|
44
|
+
const bad = (why) => new CliError("USAGE", `${file}: ${why}`, 2);
|
|
45
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw bad("expected a JSON object");
|
|
46
|
+
if (typeof body.figmaVersion !== "string" || !body.figmaVersion) throw bad("figmaVersion is required");
|
|
47
|
+
if (!Array.isArray(body.items) || body.items.length === 0) throw bad("items must list the rejected items");
|
|
48
|
+
body.items.forEach((item, n) => {
|
|
49
|
+
if (!item?.itemId && !item?.figmaNodeId) throw bad(`items[${n}] needs itemId or figmaNodeId`);
|
|
50
|
+
if (!Array.isArray(item.variants) || item.variants.length === 0) throw bad(`items[${n}] needs at least one variant`);
|
|
51
|
+
item.variants.forEach((v, m) => {
|
|
52
|
+
if (!v?.nodeId) throw bad(`items[${n}].variants[${m}] needs nodeId`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function figmaResubmit(id, file, { request = devRequest, read = (f) => readFile(f, "utf8") } = {}) {
|
|
59
|
+
if (!id || !file) throw new CliError("USAGE", "figma resubmit CHANGE_SET PAYLOAD.json", 2);
|
|
60
|
+
let text;
|
|
61
|
+
try {
|
|
62
|
+
text = await read(file);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new CliError("USAGE", `cannot read ${file}: ${error.message}`, 2);
|
|
65
|
+
}
|
|
66
|
+
const body = resubmitPayload(text, file);
|
|
67
|
+
try {
|
|
68
|
+
return await request("POST", `/api/v1/design/change-sets/${encode(id)}/resubmit`, body);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const say = RESUBMIT_REFUSALS[error?.code];
|
|
71
|
+
if (!say) throw error;
|
|
72
|
+
throw new CliError(error.code, say(id, error.details, error.message), error.exitCode ?? 1, error.details);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function figmaChangeSetShow(id, { request = devRequest } = {}) {
|
|
77
|
+
if (!id) throw new CliError("USAGE", "figma changeset show CHANGE_SET", 2);
|
|
78
|
+
try {
|
|
79
|
+
return await request("GET", `/api/v1/design/change-sets/${encode(id)}`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code !== "CHANGE_SET_NOT_FOUND") throw error;
|
|
82
|
+
throw new CliError(error.code, `No Figma change set ${id} in Dev (guideline change sets: nodii-dev design changeset show).`, error.exitCode ?? 1, error.details);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const variantLine = (v) =>
|
|
87
|
+
`${v.figmaNodeId} (${[v.beforeImageId && "before", v.afterImageId && "after"].filter(Boolean).join(", ") || "no images"})`;
|
|
88
|
+
|
|
89
|
+
/** One change set as lines: its items' round, decision, comment, the previous round's decision and comment, and variants. */
|
|
90
|
+
export function figmaChangeSetText(cs) {
|
|
91
|
+
const c = cs.itemCounts ?? {};
|
|
92
|
+
const lines = [
|
|
93
|
+
`${cs.id} ${cs.state} ${cs.title}`,
|
|
94
|
+
`file ${cs.file?.name ?? cs.fileKey} (${cs.fileKey}); holder ${cs.holder?.agent?.displayName ?? cs.holder?.agent?.id ?? "none"}`,
|
|
95
|
+
`${c.total ?? cs.items.length} items: ${c.approved ?? 0} approved, ${c.rejected ?? 0} rejected, ${c.pending ?? 0} pending`,
|
|
96
|
+
];
|
|
97
|
+
for (const item of cs.items ?? []) {
|
|
98
|
+
lines.push(`- ${item.decision} round ${item.round} ${item.figmaNodeId} ${item.entryKey ?? "(new)"} ${item.change} item ${item.id}`);
|
|
99
|
+
if (item.comment) lines.push(` comment: ${item.comment}`);
|
|
100
|
+
if (item.previousItemId)
|
|
101
|
+
lines.push(` round ${item.round - 1}: ${item.previousDecision ?? "?"}${item.previousComment ? `: ${item.previousComment}` : ""}`);
|
|
102
|
+
if (item.variants?.length) lines.push(` variants: ${item.variants.map(variantLine).join("; ")}`);
|
|
103
|
+
}
|
|
104
|
+
if (cs.previousRounds?.length) lines.push(`${cs.previousRounds.length} earlier-round item(s) replaced by a re-submit; --json lists them.`);
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
package/dist/hooks.js
CHANGED
|
@@ -102,7 +102,9 @@ export async function preHook(payload, request = devRequest) {
|
|
|
102
102
|
if (!response.data?.allowed)
|
|
103
103
|
throw new CliError(
|
|
104
104
|
"FIGMA_LOCK_DENIED",
|
|
105
|
-
response.data?.reason
|
|
105
|
+
response.data?.reason === "change-set-not-editable"
|
|
106
|
+
? `${response.data.changeSetId} is in review; writes wait for the reviewer's decisions (or nodii-dev figma withdraw ${response.data.changeSetId}).`
|
|
107
|
+
: (response.data?.reason ?? "Blocked Figma write: lock not held."),
|
|
106
108
|
2,
|
|
107
109
|
response.data,
|
|
108
110
|
);
|
package/dist/lanes.js
CHANGED
|
@@ -167,12 +167,50 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
167
167
|
throw new CliError("USAGE", `Unknown lane action: ${action}`, 2);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
/**
|
|
171
|
+
* The files a lane touches, for path-tagged rules (M05-32): its branch and
|
|
172
|
+
* working-tree changes against origin/main, plus untracked files, read from
|
|
173
|
+
* its recorded worktree. Null when that worktree cannot be read here; Dev then
|
|
174
|
+
* serves every path-tagged clause rather than miss one.
|
|
175
|
+
*/
|
|
176
|
+
export async function laneChangedPaths(lane) {
|
|
177
|
+
try {
|
|
178
|
+
const run = async (...args) => (await execFileAsync("git", ["-C", lane.worktree, ...args], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 })).stdout;
|
|
179
|
+
const base = (await run("merge-base", "origin/main", "HEAD")).trim();
|
|
180
|
+
const changed = (await run("diff", "--name-only", base)).split("\n");
|
|
181
|
+
const untracked = (await run("ls-files", "--others", "--exclude-standard")).split("\n");
|
|
182
|
+
return [...new Set([...changed, ...untracked].map((x) => x.trim()).filter(Boolean))].slice(0, 5000);
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rulesLines = (rules, key) => {
|
|
189
|
+
if (rules.error) return [`Rules: not served (${rules.error}). Until Dev serves them, read the clauses your brief cites; review delivers the rest (M05-35).`];
|
|
190
|
+
if (!rules.clauseCount) return [`Rules for this step: none served (no clause tagged for it yet, M05-37). Core: nodii-dev rules core`];
|
|
191
|
+
return [
|
|
192
|
+
`Rules for this step: ${rules.clauseCount} clause${rules.clauseCount === 1 ? "" : "s"}, ~${rules.tokens} tokens (DEC-0020). Full text: nodii-dev next ${key} --json`,
|
|
193
|
+
...rules.clauses.map((c) => ` ${c.id} ${c.gist}${c.toolGap ? ` [TOOL GAP ${c.toolGap}]` : ""}\n -> ${c.pointer}`),
|
|
194
|
+
];
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export async function nextCommand(ref, flags, { request = devRequest, changedPaths = laneChangedPaths } = {}) {
|
|
171
198
|
const lane = (await request("GET", `/api/v1/lanes/${encode(ref)}`)).data;
|
|
172
199
|
const pending = lane.steps.filter((x) => ["waiting", "current", "blocked"].includes(x.state)).sort((a, b) => a.position - b.position);
|
|
173
200
|
const steps = ["backend", "frontend"].map((side) => pending.find((x) => x.side === side)).filter(Boolean);
|
|
174
201
|
const data = steps.map((step) => ({ side: step.side, step: step.stepKey, label: step.label, state: step.state, command: `nodii-dev lane step ${lane.key} --side ${step.side} --${step.state === "current" ? "done" : "start"} ${step.stepKey}` }));
|
|
175
|
-
|
|
202
|
+
// M05-32: the clauses Dev serves for this step. A failure here never hides the next step.
|
|
203
|
+
let rules = null;
|
|
204
|
+
if (data.length) {
|
|
205
|
+
const paths = flags.paths ? list(flags.paths) : await changedPaths(lane);
|
|
206
|
+
const body = { ...(paths ? { paths } : {}), ...(flags.components ? { components: list(flags.components) } : {}), ...(flags.role ? { role: String(flags.role) } : {}) };
|
|
207
|
+
try { rules = (await request("POST", `/api/v1/lanes/${encode(lane.key)}/rules`, body)).data; }
|
|
208
|
+
catch (error) { rules = { error: error.code ?? error.message }; }
|
|
209
|
+
}
|
|
210
|
+
const human = data.length
|
|
211
|
+
? [...data.map((x) => `${x.side}: ${x.label}\n ${x.command}`), ...rulesLines(rules, lane.key)].join("\n")
|
|
212
|
+
: `All workflow steps are complete. Run: nodii-dev lane land ${lane.key}`;
|
|
213
|
+
return { payload: { data, ...(rules ? { rules } : {}) }, human };
|
|
176
214
|
}
|
|
177
215
|
|
|
178
216
|
export async function builtCommand(action, args, flags, { request = devRequest } = {}) {
|
package/dist/upload.js
CHANGED
|
@@ -273,7 +273,8 @@ export async function rulebookCommand(action, args, flags, { request = devReques
|
|
|
273
273
|
if (flags[name] === true) throw new CliError("USAGE", `--${name} needs a version, e.g. --${name} 1.0`, 2);
|
|
274
274
|
if (action === "list") {
|
|
275
275
|
const payload = await request("GET", `/api/v1/documents${query({ section: flags.section ?? (flags.all ? undefined : "rulebook"), kind: flags.kind, serviceId: flags.service, domain: flags.domain, status: flags.status, q: flags.q, limit: flags.limit ?? 500, offset: flags.offset })}`);
|
|
276
|
-
|
|
276
|
+
// Dev issue 80: the clause count, so a document Dev reads as 0/44 shows at a glance.
|
|
277
|
+
return { payload, human: (p) => p.data.map((x) => `${x.code}\t${x.currentVersion}\t${x.status}\t${x.kind}\t${x.clauseCount ?? "?"} clause${x.clauseCount === 1 ? "" : "s"}\t${x.title}${x.parkedCount ? `\tparked=${x.parkedCount}` : ""}`).join("\n") || "No documents." };
|
|
277
278
|
}
|
|
278
279
|
if (action === "show") {
|
|
279
280
|
const code = args[0];
|
|
@@ -289,3 +290,43 @@ export async function rulebookCommand(action, args, flags, { request = devReques
|
|
|
289
290
|
}
|
|
290
291
|
throw new CliError("USAGE", "rulebook list|show|diff", 2);
|
|
291
292
|
}
|
|
293
|
+
|
|
294
|
+
const pad = (v, n) => String(v).padStart(n);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* `nodii-dev rules core|report|cap` (DEC-0020; M05-33, M05-36): the generated
|
|
298
|
+
* core, the delivery report, and the operator's core token cap.
|
|
299
|
+
*/
|
|
300
|
+
export async function rulesCommand(action, args, flags, { request = devRequest } = {}) {
|
|
301
|
+
if (action === "core") {
|
|
302
|
+
// Dev refuses a core over the cap with CORE_OVER_CAP; that error is the output.
|
|
303
|
+
const payload = await request("GET", "/api/v1/rules/core");
|
|
304
|
+
return { payload, human: (p) => `${p.data.markdown.trimEnd()}\n\n${p.data.clauseCount} core clause${p.data.clauseCount === 1 ? "" : "s"}, ~${p.data.tokens} of ${p.data.cap} tokens.` };
|
|
305
|
+
}
|
|
306
|
+
if (action === "report") {
|
|
307
|
+
const payload = await request("GET", `/api/v1/rules/report${query({ section: flags.section, lane: flags.lane })}`);
|
|
308
|
+
return {
|
|
309
|
+
payload,
|
|
310
|
+
human: (p) => {
|
|
311
|
+
const d = p.data;
|
|
312
|
+
const row = (code, t) => `${code.padEnd(10)}${pad(t.clauses, 6)}${pad(t.must, 6)}${pad(t.enforced, 9)}${pad(t.served, 7)}${pad(t.core, 5)}${pad(t.untagged, 9)}${pad(t.untaggedMust, 14)}${pad(t.toolGap, 9)}`;
|
|
313
|
+
const lines = [
|
|
314
|
+
`${"document".padEnd(10)}${pad("all", 6)}${pad("MUST", 6)}${pad("enforced", 9)}${pad("served", 7)}${pad("core", 5)}${pad("untagged", 9)}${pad("untagged MUST", 14)}${pad("tool gap", 9)}`,
|
|
315
|
+
...(flags.lane ? [] : d.documents.map((x) => row(x.code, x))),
|
|
316
|
+
row("TOTAL", d.totals),
|
|
317
|
+
`Core: ${d.core.clauseCount} clauses, ~${d.core.tokens} of ${d.core.cap} tokens${d.core.overCap ? " (OVER THE CAP)" : ""}.`,
|
|
318
|
+
...(d.lanes.length ? ["Served per lane:"] : ["No lane has been served rules yet."]),
|
|
319
|
+
...d.lanes.flatMap((l) => [` ${l.key} (${l.status}): ${l.clauseCount} clauses, ~${l.tokens} tokens`, ...l.steps.map((s) => ` ${s.side}/${s.step}: ${s.clauseCount} clauses, ~${s.tokens} tokens, served ${s.servedCount}x`)]),
|
|
320
|
+
];
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (action === "cap") {
|
|
326
|
+
const cap = Number(args[0]);
|
|
327
|
+
if (!Number.isInteger(cap) || cap < 100 || cap > 100000) throw new CliError("USAGE", "rules cap needs a whole number of tokens, 100 to 100000 (M05-33)", 2);
|
|
328
|
+
const payload = await request("PUT", "/api/v1/rules/core/cap", { cap });
|
|
329
|
+
return { payload, human: (p) => `Core cap: ${p.data.previous} -> ${p.data.cap} tokens.` };
|
|
330
|
+
}
|
|
331
|
+
throw new CliError("USAGE", "rules core | rules report [--section rulebook|services|all] [--lane KEY] | rules cap TOKENS", 2);
|
|
332
|
+
}
|