@lumoai/cli 1.44.0 → 1.45.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.
|
@@ -112,8 +112,9 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
112
112
|
|
|
113
113
|
- **Header** — task identifier/title/status + `verification round N/3` (round 0 = never verified) + an escalation warning when the machine loop is exhausted.
|
|
114
114
|
- **Machine verification rollup** (LUM-470) — directly under the `Criteria` header, one line `Machine verification: N machine-verified / M human override (of T MACHINE criteria)` over the active MACHINE criteria, aligned with the web read model (LUM-456). Printed whenever the contract has ≥1 MACHINE criterion, so the terminal rollup never reads as all-human when a checkpointer actually verified the work.
|
|
115
|
-
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its checkpointer and latest verdict line (
|
|
115
|
+
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its checkpointer and latest verdict line (failure tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
|
|
116
116
|
- A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict (LUM-470): `· machine-verified` when a checkpointer actually passed it (even after a human later signs the task off), or `· human override (no machine pass)` when it passes only on a human sign-off with no machine run underneath. This keeps the terminal honest with web — a machine-verified criterion that a human co-signed no longer reads as a plain human pass.
|
|
117
|
+
- A verdict's **evidence is drillable** (LUM-563), rendered as an indented `↳ evidence:` line under the verdict (PASS _and_ FAIL) instead of the inert raw pointer that used to ride the verdict line — so a conclusion points at real proof you can act on, not just the `check:` command: a `cmd:` pointer prints the actual command + exit code (`ran \`…\` → exit N · re-run to reproduce`), a `file:`pointer prints a terminal-clickable`path:line`, and a `commit:` pointer prints a navigable web URL (`<repo>/commit/<hash>`, resolved from the local git `origin`remote) or a`git show <hash>`fallback when no remote resolves. A criterion that **requires evidence but has none recorded yet** (e.g. a HUMAN evidence criterion before sign-off) renders an explicit`↳ evidence: pending — no reference recorded yet`(fail-closed) instead of a bare, dead`[evidence]` tag.
|
|
117
118
|
- A pass can carry a **`⚠ pre-edit version`** note (LUM-457): the criterion was changed after that verdict (reworded, or its checkpointer was swapped so the recorded evidence ran a different command). The pass still counts as met (a stale pass does not block DONE — render-only signal), but it vouches for an older version — **re-run `lumo verify` to re-confirm against the current criterion.** This is the habit whenever you edit a MACHINE criterion's checkpointer mid-task: change the check, then re-verify so the green is honest.
|
|
118
119
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
119
120
|
- **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
|
|
@@ -127,6 +128,8 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
127
128
|
|
|
128
129
|
When the trail is genuinely empty it states the **basis** (`None recorded — N rounds run, 0 FAIL, no send-backs, no reopens, no leftover follow-ups`); when nothing has been verified yet it says so (`No verification has run yet — cannot confirm there were no difficulties`) rather than rendering an implicitly-clean slate. Carried in `--json` as `struggleTrail` (incl. `pullRequests` + `reopens`).
|
|
129
130
|
|
|
131
|
+
- **Trend** (LUM-562) — 规律 7 趋势非快照: the _movement_ of the key quantities across the task's attempts, not a single snapshot. Where History/Cost/Struggle list current values, this shows direction: **Pass rate** across verification rounds (`r1 60% → r2 100% (↑ +40pts)`), **Cost/session** across the task's sessions (`4.2K → 1.1K tokens (↓), 5.3K total` — per-session spend from the same source as the **Cost** total, so the trajectory's points sum to it), and **Rework** accrual (`3 accrued — 1 FAIL round, 1 reopen, +1 PR cycle (↑ from 0)`). **Honest about a single point:** with only one round and one session every quantity is one data point, so it prints `Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions)` rather than drawing a fake arrow off one value (closes the named "1-round pass = single point" gap). When nothing was verified and no cost was measured it says `No verification rounds or measured cost yet — nothing to trend`. Carried in `--json` as `trend { passRate[], cost[], rework{} }`. Omitted only against an older server.
|
|
132
|
+
|
|
130
133
|
- **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included). This list IS the plan — recomputed from the event log on every read, never maintained separately. Empty + rounds recorded = awaiting human adjudication.
|
|
131
134
|
- **Open boundary crossings** (LUM-448) — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count, then one line per crossing `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first), each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (LUM-469 — who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
|
|
132
135
|
- **Read-only awareness** — this surfaces crossings detected elsewhere (LUM-426/435/442); there is no CLI path to disposition or clear one. Disposition stays web + human-only (LUM-426/435/422): an agent/CLI bearer cannot clear its own crossing from the terminal.
|
|
@@ -7,6 +7,7 @@ const api_1 = require("../lib/api");
|
|
|
7
7
|
const resolve_bound_task_1 = require("../lib/resolve-bound-task");
|
|
8
8
|
const sanitize_1 = require("../lib/sanitize");
|
|
9
9
|
const open_crossings_1 = require("../lib/open-crossings");
|
|
10
|
+
const evidence_display_1 = require("../lib/evidence-display");
|
|
10
11
|
/** One-line a possibly-multiline crossing detail and cap it so the safety block
|
|
11
12
|
* stays a glance, never a wall (it must not overshadow the criteria). */
|
|
12
13
|
const CROSSING_DETAIL_CAP = 160;
|
|
@@ -85,9 +86,6 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
85
86
|
lines.push(` ✗ FAIL@r${v.round}${why}`);
|
|
86
87
|
}
|
|
87
88
|
else {
|
|
88
|
-
const evidencePart = v.evidencePointer
|
|
89
|
-
? ` · ${(0, sanitize_1.sanitizeField)(v.evidencePointer)}`
|
|
90
|
-
: '';
|
|
91
89
|
// LUM-470: tag a passing MACHINE criterion by the read model's
|
|
92
90
|
// machinePassed flag, not the latest verdict — a criterion a checkpointer
|
|
93
91
|
// verified reads as machine-verified even after a human signs off, and a
|
|
@@ -97,13 +95,26 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
97
95
|
? ' · machine-verified'
|
|
98
96
|
: ' · human override (no machine pass)'
|
|
99
97
|
: '';
|
|
100
|
-
lines.push(` ✓ ${v.verdict}@r${v.round}${
|
|
98
|
+
lines.push(` ✓ ${v.verdict}@r${v.round}${machineTag}`);
|
|
101
99
|
// LUM-457: a pass that vouches for a pre-edit version of the criterion —
|
|
102
100
|
// render-only downgrade, the criterion still counts met.
|
|
103
101
|
if (c.verdictStale || c.checkMismatch) {
|
|
104
102
|
lines.push(' ⚠ pre-edit version — criterion changed since this check; re-run `lumo verify` to re-confirm');
|
|
105
103
|
}
|
|
106
104
|
}
|
|
105
|
+
// LUM-563: drill the verdict's evidence into a navigable / re-runnable
|
|
106
|
+
// reference instead of the inert raw pointer that used to ride the verdict
|
|
107
|
+
// line — a commit URL (or `git show` fallback), a clickable path:line, or
|
|
108
|
+
// the actual command + exit. Rendered for PASS and FAIL alike so a failure
|
|
109
|
+
// is just as reproducible. When a criterion requires evidence but none is
|
|
110
|
+
// recorded yet, say so explicitly (fail-closed) rather than leaving the
|
|
111
|
+
// bare `[evidence]` tag pointing at nothing.
|
|
112
|
+
if (v?.evidencePointer) {
|
|
113
|
+
lines.push(` ↳ evidence: ${(0, sanitize_1.sanitizeField)((0, evidence_display_1.formatEvidenceDrilldown)(v.evidencePointer, extras.repoWebUrl ?? null))}`);
|
|
114
|
+
}
|
|
115
|
+
else if (c.evidenceRequired) {
|
|
116
|
+
lines.push(' ↳ evidence: pending — no reference recorded yet');
|
|
117
|
+
}
|
|
107
118
|
// LUM-511 Phase 5: send-back lifecycle (was this criterion's send-back
|
|
108
119
|
// resolved, and by which PR).
|
|
109
120
|
const sb = c.sendBackResolution;
|
|
@@ -136,6 +147,7 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
136
147
|
}
|
|
137
148
|
pushCost(lines, data);
|
|
138
149
|
pushStruggleTrail(lines, data);
|
|
150
|
+
pushTrend(lines, data);
|
|
139
151
|
lines.push('');
|
|
140
152
|
if (data.nextActions.length === 0) {
|
|
141
153
|
lines.push(data.currentRound > 0
|
|
@@ -291,6 +303,95 @@ function pushStruggleTrail(lines, data) {
|
|
|
291
303
|
}
|
|
292
304
|
}
|
|
293
305
|
}
|
|
306
|
+
/** Direction glyph for a delta — ↑ up, ↓ down, → flat. */
|
|
307
|
+
function arrow(delta) {
|
|
308
|
+
return delta > 0 ? '↑' : delta < 0 ? '↓' : '→';
|
|
309
|
+
}
|
|
310
|
+
/** Pass rate of one round as an integer percent (0 verdicts → 0%). */
|
|
311
|
+
function passPct(p) {
|
|
312
|
+
const total = p.passed + p.failed;
|
|
313
|
+
return total === 0 ? 0 : Math.round((p.passed / total) * 100);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Append the "Trend" section (LUM-562, 規律7 趨勢非快照) — the *movement* of the
|
|
317
|
+
* task's key quantities across its attempts, not a single-point snapshot. Where
|
|
318
|
+
* History lists each round's PASS/FAIL, Cost shows the running total, and
|
|
319
|
+
* Struggle lists the scars, this shows direction: pass rate climbing, cost per
|
|
320
|
+
* session rising/falling, rework accruing. The honesty rule (mirrors the
|
|
321
|
+
* struggle trail): a single data point is NOT a trajectory — with one round and
|
|
322
|
+
* one session the section says so outright rather than drawing a fake arrow off
|
|
323
|
+
* a single value, closing the named "1-round pass = single point" gap. The
|
|
324
|
+
* per-session cost reuses the same source as the Cost section, so the
|
|
325
|
+
* trajectory's points sum to the Cost total (no in-report drift). Skipped only
|
|
326
|
+
* when the server didn't emit the field (older server) — never fabricated.
|
|
327
|
+
*/
|
|
328
|
+
function pushTrend(lines, data) {
|
|
329
|
+
const trend = data.trend;
|
|
330
|
+
if (!trend)
|
|
331
|
+
return; // older server: no trend data — don't invent one.
|
|
332
|
+
const rounds = trend.passRate;
|
|
333
|
+
const cost = trend.cost;
|
|
334
|
+
const rw = trend.rework;
|
|
335
|
+
lines.push('');
|
|
336
|
+
// Nothing to trend at all — no verification, no measured cost.
|
|
337
|
+
if (rounds.length === 0 && cost.length === 0) {
|
|
338
|
+
lines.push('Trend:');
|
|
339
|
+
lines.push(' No verification rounds or measured cost yet — nothing to trend. Run `lumo verify`.');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// A trajectory needs ≥2 points in at least one dimension. With a single
|
|
343
|
+
// round AND a single session, every quantity is one data point — say so
|
|
344
|
+
// plainly instead of implying a flat/healthy trend (the named gap).
|
|
345
|
+
const hasTrajectory = rounds.length >= 2 || cost.length >= 2;
|
|
346
|
+
if (!hasTrajectory) {
|
|
347
|
+
lines.push('Trend:');
|
|
348
|
+
lines.push(' Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions).');
|
|
349
|
+
const bits = [];
|
|
350
|
+
if (rounds.length === 1)
|
|
351
|
+
bits.push(`pass rate r${rounds[0].round} ${passPct(rounds[0])}%`);
|
|
352
|
+
if (cost.length === 1)
|
|
353
|
+
bits.push(`cost ${fmtTokens(cost[0].total)} tokens`);
|
|
354
|
+
bits.push(rw.total === 0 ? 'rework none' : `rework ${rw.total}`);
|
|
355
|
+
lines.push(` ${bits.join(' · ')}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const sessionWord = cost.length === 1 ? 'session' : 'sessions';
|
|
359
|
+
lines.push(`Trend (${rounds.length} rounds · ${cost.length} ${sessionWord}):`);
|
|
360
|
+
// Pass rate across rounds — the climb (or fall) of the verify loop.
|
|
361
|
+
if (rounds.length >= 2) {
|
|
362
|
+
const seq = rounds.map(r => `r${r.round} ${passPct(r)}%`).join(' → ');
|
|
363
|
+
const delta = passPct(rounds[rounds.length - 1]) - passPct(rounds[0]);
|
|
364
|
+
const sign = delta > 0 ? `+${delta}` : `${delta}`;
|
|
365
|
+
lines.push(` Pass rate: ${seq} (${arrow(delta)} ${sign}pts)`);
|
|
366
|
+
}
|
|
367
|
+
else if (rounds.length === 1) {
|
|
368
|
+
lines.push(` Pass rate: r${rounds[0].round} ${passPct(rounds[0])}% (1 round — single point)`);
|
|
369
|
+
}
|
|
370
|
+
// Cost per session across the task's sessions — the spend trajectory.
|
|
371
|
+
if (cost.length >= 2) {
|
|
372
|
+
const first = cost[0].total;
|
|
373
|
+
const last = cost[cost.length - 1].total;
|
|
374
|
+
const total = cost.reduce((a, c) => a + c.total, 0);
|
|
375
|
+
lines.push(` Cost/session: ${fmtTokens(first)} → ${fmtTokens(last)} tokens (${arrow(last - first)}), ${fmtTokens(total)} total over ${cost.length} sessions`);
|
|
376
|
+
}
|
|
377
|
+
else if (cost.length === 1) {
|
|
378
|
+
lines.push(` Cost: ${fmtTokens(cost[0].total)} tokens (1 session — single point)`);
|
|
379
|
+
}
|
|
380
|
+
// Rework accrual — monotonic from a clean baseline of 0.
|
|
381
|
+
if (rw.total === 0) {
|
|
382
|
+
lines.push(' Rework: none accrued (0) — clean across every attempt');
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const parts = [];
|
|
386
|
+
if (rw.reworkRounds > 0)
|
|
387
|
+
parts.push(`${rw.reworkRounds} FAIL round${rw.reworkRounds === 1 ? '' : 's'}`);
|
|
388
|
+
if (rw.reopens > 0)
|
|
389
|
+
parts.push(`${rw.reopens} reopen${rw.reopens === 1 ? '' : 's'}`);
|
|
390
|
+
if (rw.extraPrCycles > 0)
|
|
391
|
+
parts.push(`+${rw.extraPrCycles} PR cycle${rw.extraPrCycles === 1 ? '' : 's'}`);
|
|
392
|
+
lines.push(` Rework: ${rw.total} accrued — ${parts.join(', ')} (↑ from 0)`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
294
395
|
/**
|
|
295
396
|
* Append the OPEN boundary-crossings safety block (LUM-448) — a count, one line
|
|
296
397
|
* per crossing with its severity + category + clipped detail, and a pointer to
|
|
@@ -407,5 +508,9 @@ async function taskStatus(identifier, options = {}) {
|
|
|
407
508
|
process.stdout.write(formatTaskStatus(data, {
|
|
408
509
|
openCrossings: crossingsResult,
|
|
409
510
|
dispositionUrl: (0, open_crossings_1.dispositionUrl)(base, creds.workspaceSlug ?? 'lumo', data.task.identifier),
|
|
511
|
+
// LUM-563: resolve the local repo's web base so a `commit:` evidence
|
|
512
|
+
// pointer drills to a navigable URL; null (not a git repo / no origin)
|
|
513
|
+
// falls back to a `git show` hint in the renderer.
|
|
514
|
+
repoWebUrl: (0, evidence_display_1.resolveRepoWebUrl)(),
|
|
410
515
|
}));
|
|
411
516
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeRepoWebUrl = normalizeRepoWebUrl;
|
|
4
|
+
exports.resolveRepoWebUrl = resolveRepoWebUrl;
|
|
5
|
+
exports.formatEvidenceDrilldown = formatEvidenceDrilldown;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const acceptance_evidence_1 = require("../../../shared/src/acceptance-evidence");
|
|
8
|
+
/**
|
|
9
|
+
* Drill-down rendering for acceptance evidence pointers (LUM-563).
|
|
10
|
+
*
|
|
11
|
+
* A verdict's evidencePointer is a structured token (`cmd:…#exit=N`,
|
|
12
|
+
* `file:path:line`, `commit:hash`). Printed raw it satisfies Norman's gulf of
|
|
13
|
+
* evaluation only halfway: you can read it but not act on it. These helpers
|
|
14
|
+
* turn each shape into a navigable / re-runnable line — a web commit URL when
|
|
15
|
+
* the git remote resolves (else a `git show` fallback), a terminal-clickable
|
|
16
|
+
* `path:line`, or the actual command + exit you can re-run to reproduce.
|
|
17
|
+
*
|
|
18
|
+
* No third-party deps; sanitization is applied by the caller (task-status.ts)
|
|
19
|
+
* before anything reaches the terminal.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a git remote URL to its https web base (no trailing `.git`), or
|
|
23
|
+
* null when it doesn't look like a remote we can navigate. Fails closed: a
|
|
24
|
+
* shape we can't confidently map yields null so the caller drops to a
|
|
25
|
+
* non-URL fallback rather than printing a broken link.
|
|
26
|
+
*
|
|
27
|
+
* Handles the three remote forms git emits:
|
|
28
|
+
* - scp-style `git@host:owner/repo.git`
|
|
29
|
+
* - https `https://host/owner/repo.git`
|
|
30
|
+
* - ssh url `ssh://git@host/owner/repo.git`
|
|
31
|
+
*/
|
|
32
|
+
function normalizeRepoWebUrl(remote) {
|
|
33
|
+
const raw = remote.trim();
|
|
34
|
+
if (!raw)
|
|
35
|
+
return null;
|
|
36
|
+
let host;
|
|
37
|
+
let path;
|
|
38
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(raw);
|
|
39
|
+
if (scp) {
|
|
40
|
+
host = scp[1];
|
|
41
|
+
path = scp[2];
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (url.protocol !== 'https:' && url.protocol !== 'ssh:')
|
|
52
|
+
return null;
|
|
53
|
+
host = url.host;
|
|
54
|
+
path = url.pathname;
|
|
55
|
+
}
|
|
56
|
+
const cleanPath = path
|
|
57
|
+
.replace(/^\/+/, '')
|
|
58
|
+
.replace(/\.git$/, '')
|
|
59
|
+
.replace(/\/+$/, '');
|
|
60
|
+
// A navigable repo path is at minimum owner/repo — reject a bare host.
|
|
61
|
+
if (!host || !cleanPath || !cleanPath.includes('/'))
|
|
62
|
+
return null;
|
|
63
|
+
return `https://${host}/${cleanPath}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the current repo's web base by reading `origin`'s remote URL. Best
|
|
67
|
+
* effort and side-effecting (runs git in the cwd), so it lives outside the pure
|
|
68
|
+
* formatter and is injected via TaskStatusExtras. Null when not a git repo /
|
|
69
|
+
* no origin / unparseable — the renderer then uses the `git show` fallback.
|
|
70
|
+
*/
|
|
71
|
+
function resolveRepoWebUrl(cwd = process.cwd()) {
|
|
72
|
+
try {
|
|
73
|
+
const r = (0, child_process_1.spawnSync)('git', ['remote', 'get-url', 'origin'], {
|
|
74
|
+
cwd,
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
timeout: 5_000,
|
|
77
|
+
});
|
|
78
|
+
if (r.status !== 0 || !r.stdout)
|
|
79
|
+
return null;
|
|
80
|
+
return normalizeRepoWebUrl(r.stdout);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Render the actionable evidence body for one pointer (the text after
|
|
88
|
+
* `↳ evidence: `). Unrecognised shapes fall back to the raw pointer so data is
|
|
89
|
+
* never hidden, only upgraded when we know how.
|
|
90
|
+
*/
|
|
91
|
+
function formatEvidenceDrilldown(pointer, repoWebUrl) {
|
|
92
|
+
const parsed = (0, acceptance_evidence_1.parseEvidencePointer)(pointer);
|
|
93
|
+
if (!parsed)
|
|
94
|
+
return pointer;
|
|
95
|
+
if (parsed.kind === 'cmd') {
|
|
96
|
+
return `ran \`${parsed.command}\` → exit ${parsed.exitCode} · re-run to reproduce`;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.kind === 'file') {
|
|
99
|
+
return `${parsed.path}:${parsed.line}`;
|
|
100
|
+
}
|
|
101
|
+
// commit
|
|
102
|
+
const short = parsed.hash.slice(0, 10);
|
|
103
|
+
return repoWebUrl
|
|
104
|
+
? `commit ${short} · ${repoWebUrl}/commit/${parsed.hash}`
|
|
105
|
+
: `commit ${short} · view with \`git show ${parsed.hash}\``;
|
|
106
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EVIDENCE_POINTER_MAX = exports.EVIDENCE_POINTER_FORMAT_HINT = exports.EVIDENCE_POINTER_PATTERNS = void 0;
|
|
4
4
|
exports.isValidEvidencePointer = isValidEvidencePointer;
|
|
5
|
+
exports.parseEvidencePointer = parseEvidencePointer;
|
|
5
6
|
exports.buildCmdEvidencePointer = buildCmdEvidencePointer;
|
|
6
7
|
/**
|
|
7
8
|
* Evidence-pointer grammar for acceptance verification (LUM-343).
|
|
@@ -29,6 +30,24 @@ function isValidEvidencePointer(value) {
|
|
|
29
30
|
}
|
|
30
31
|
/** Max stored pointer length — mirrors the column-level cap in validation. */
|
|
31
32
|
exports.EVIDENCE_POINTER_MAX = 2_000;
|
|
33
|
+
/**
|
|
34
|
+
* Decompose an evidence pointer into its parts, or null if it doesn't parse.
|
|
35
|
+
* The grammar is the single source of truth (EVIDENCE_POINTER_PATTERNS); this
|
|
36
|
+
* mirrors those exact shapes — a `#` may appear inside a `cmd:` command, so the
|
|
37
|
+
* exit marker is matched greedily from the END, never the first `#`.
|
|
38
|
+
*/
|
|
39
|
+
function parseEvidencePointer(value) {
|
|
40
|
+
const commit = /^commit:([0-9a-f]{7,40})$/.exec(value);
|
|
41
|
+
if (commit)
|
|
42
|
+
return { kind: 'commit', hash: commit[1] };
|
|
43
|
+
const file = /^file:([^\s]+):(\d+(?:-\d+)?)$/.exec(value);
|
|
44
|
+
if (file)
|
|
45
|
+
return { kind: 'file', path: file[1], line: file[2] };
|
|
46
|
+
const cmd = /^cmd:([\s\S]+)#exit=(\d+)$/.exec(value);
|
|
47
|
+
if (cmd)
|
|
48
|
+
return { kind: 'cmd', command: cmd[1], exitCode: Number(cmd[2]) };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* Build a `cmd:` evidence pointer for an executed checkpointer. The command
|
|
34
53
|
* is truncated so the suffix (`#exit=N`) always survives the length cap —
|