@outerlayer/cli 0.33.0-next.20261022124145 → 0.33.0-next.20261022160949
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/dist/build-info.json +1 -1
- package/dist/{emit-criteria-cmd-IC5OIZX2.js → chunk-J42MMS4R.js} +15 -34
- package/dist/{cli-5JSVOAE5.js → cli-OHQKHGEU.js} +2 -2
- package/dist/emit-criteria-cmd-6D4D5ZBW.js +33 -0
- package/dist/index.js +1 -1
- package/dist/{spec-cmd-SLJT7MRX.js → spec-cmd-ZJAPP2XZ.js} +56 -2
- package/package.json +1 -1
- package/skill-pack/maintained/outerlayer-build/SKILL.md +8 -4
- package/skill-pack/maintained/outerlayer-build/references/criteria-judge.md +6 -4
- package/skill-pack/maintained/outerlayer-build/scripts/record-criteria.mjs +64 -8
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"2026-10-
|
|
1
|
+
{"id":"2026-10-22T16:11:53.196Z+8818aa0","root":"/home/runner/_work/outerlayer/outerlayer","pkg":"outerlayer"}
|
|
@@ -2,32 +2,13 @@
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { launchRefusalForSession } from './chunk-ESVW4MJY.js';
|
|
4
4
|
import { detectActiveSession, autoConnect } from './chunk-C3XLVHN6.js';
|
|
5
|
-
import './chunk-B3EE7OKK.js';
|
|
6
|
-
import './chunk-EWVYQLD3.js';
|
|
7
5
|
import { resolveRepoIdentity } from './chunk-5DNCFOXA.js';
|
|
8
|
-
import './chunk-HJYBWJAS.js';
|
|
9
|
-
import './chunk-NSPRS7WK.js';
|
|
10
|
-
import './chunk-G2JWZDFA.js';
|
|
11
6
|
import { C } from './chunk-ANPNT6PM.js';
|
|
12
7
|
import { resolveGatewayCredentials, defaultFetch, unauthorizedMessage } from './chunk-VQ2JGZSC.js';
|
|
13
|
-
import './chunk-PJDQPF5N.js';
|
|
14
|
-
import './chunk-JT3YO5MH.js';
|
|
15
|
-
import './chunk-ESP5VZBD.js';
|
|
16
|
-
import './chunk-HDSDUTX4.js';
|
|
17
|
-
import './chunk-BJ3KTMDH.js';
|
|
18
|
-
import './chunk-G63KT5I5.js';
|
|
19
|
-
import './chunk-5BHIBJDA.js';
|
|
20
|
-
import './chunk-CDDED64E.js';
|
|
21
|
-
import './chunk-RA3O54FC.js';
|
|
22
8
|
import { launchedItemForSession } from './chunk-T2Q7ISPE.js';
|
|
23
9
|
import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
|
|
24
|
-
import './chunk-CRB3POP4.js';
|
|
25
|
-
import './chunk-LJG6EMLJ.js';
|
|
26
10
|
import { CriteriaListSchema } from './chunk-E6CO3MJE.js';
|
|
27
11
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
28
|
-
import './chunk-XD4ZCMA4.js';
|
|
29
|
-
import './chunk-MESOOLF4.js';
|
|
30
|
-
import './chunk-Z5GEFMRW.js';
|
|
31
12
|
import { readFileSync } from 'fs';
|
|
32
13
|
|
|
33
14
|
createRequire(import.meta.url);
|
|
@@ -129,11 +110,7 @@ async function errorCode(response) {
|
|
|
129
110
|
}
|
|
130
111
|
return { code, detail: text.replace(/\s+/g, " ").trim().slice(0, 160) };
|
|
131
112
|
}
|
|
132
|
-
async function
|
|
133
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
134
|
-
const home = opts.home ?? homeFromEnv();
|
|
135
|
-
const env = opts.env ?? process.env;
|
|
136
|
-
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
113
|
+
async function readListFile(opts) {
|
|
137
114
|
let text;
|
|
138
115
|
if (opts.file === "-") {
|
|
139
116
|
text = await (opts.readStdin ?? readAllStdin)();
|
|
@@ -144,21 +121,25 @@ async function runEmitCriteria(opts) {
|
|
|
144
121
|
throw new EmitCriteriaError(`cannot read "${opts.file}": ${String(err)}`);
|
|
145
122
|
}
|
|
146
123
|
}
|
|
147
|
-
let raw;
|
|
148
124
|
if (opts.file.endsWith(".md")) {
|
|
149
125
|
const criteria = criteriaFromAcceptanceFile(text);
|
|
150
126
|
if (criteria.length === 0) {
|
|
151
127
|
throw new EmitCriteriaError(`"${opts.file}" lists no criteria: none of its list items opens with an id such as \`AC-123-01\``);
|
|
152
128
|
}
|
|
153
|
-
|
|
154
|
-
} else {
|
|
155
|
-
try {
|
|
156
|
-
raw = JSON.parse(text);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
throw new EmitCriteriaError(`"${opts.file}" is not valid JSON: ${String(err)}`);
|
|
159
|
-
}
|
|
129
|
+
return { criteria };
|
|
160
130
|
}
|
|
161
|
-
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(text);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
throw new EmitCriteriaError(`"${opts.file}" is not valid JSON: ${String(err)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function runEmitCriteria(opts) {
|
|
138
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
139
|
+
const home = opts.home ?? homeFromEnv();
|
|
140
|
+
const env = opts.env ?? process.env;
|
|
141
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
142
|
+
const list = parseFile(opts.list ?? await readListFile(opts));
|
|
162
143
|
const identity = resolveRepoIdentity(cwd);
|
|
163
144
|
const sessionId = detectActiveSession({
|
|
164
145
|
env,
|
|
@@ -212,4 +193,4 @@ async function runEmitCriteria(opts) {
|
|
|
212
193
|
return { data, output, exitCode: 0 };
|
|
213
194
|
}
|
|
214
195
|
|
|
215
|
-
export { EmitCriteriaError, runEmitCriteria };
|
|
196
|
+
export { EmitCriteriaError, criteriaFromAcceptanceFile, runEmitCriteria };
|
|
@@ -4601,7 +4601,7 @@ of the item, and a (proof: <kind>) annotation names its proof.
|
|
|
4601
4601
|
${cliInvocation()} emit criteria acceptance/412-login.md --item 412
|
|
4602
4602
|
`
|
|
4603
4603
|
).action(async (file, opts) => {
|
|
4604
|
-
const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-
|
|
4604
|
+
const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-6D4D5ZBW.js');
|
|
4605
4605
|
try {
|
|
4606
4606
|
const result = await runEmitCriteria({
|
|
4607
4607
|
file,
|
|
@@ -4627,7 +4627,7 @@ function registerSpecCommands(program2) {
|
|
|
4627
4627
|
const run = async (action) => {
|
|
4628
4628
|
const { WorkCommandError } = await import('./work-cmd-DYM67Z3F.js');
|
|
4629
4629
|
try {
|
|
4630
|
-
await action(await import('./spec-cmd-
|
|
4630
|
+
await action(await import('./spec-cmd-ZJAPP2XZ.js'));
|
|
4631
4631
|
} catch (err) {
|
|
4632
4632
|
if (err instanceof WorkCommandError) {
|
|
4633
4633
|
fail(err.message);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { EmitCriteriaError, runEmitCriteria } from './chunk-J42MMS4R.js';
|
|
4
|
+
import './chunk-ESVW4MJY.js';
|
|
5
|
+
import './chunk-C3XLVHN6.js';
|
|
6
|
+
import './chunk-B3EE7OKK.js';
|
|
7
|
+
import './chunk-EWVYQLD3.js';
|
|
8
|
+
import './chunk-5DNCFOXA.js';
|
|
9
|
+
import './chunk-HJYBWJAS.js';
|
|
10
|
+
import './chunk-NSPRS7WK.js';
|
|
11
|
+
import './chunk-G2JWZDFA.js';
|
|
12
|
+
import './chunk-ANPNT6PM.js';
|
|
13
|
+
import './chunk-VQ2JGZSC.js';
|
|
14
|
+
import './chunk-PJDQPF5N.js';
|
|
15
|
+
import './chunk-JT3YO5MH.js';
|
|
16
|
+
import './chunk-ESP5VZBD.js';
|
|
17
|
+
import './chunk-HDSDUTX4.js';
|
|
18
|
+
import './chunk-BJ3KTMDH.js';
|
|
19
|
+
import './chunk-G63KT5I5.js';
|
|
20
|
+
import './chunk-5BHIBJDA.js';
|
|
21
|
+
import './chunk-CDDED64E.js';
|
|
22
|
+
import './chunk-RA3O54FC.js';
|
|
23
|
+
import './chunk-T2Q7ISPE.js';
|
|
24
|
+
import './chunk-WZBHBH3J.js';
|
|
25
|
+
import './chunk-CRB3POP4.js';
|
|
26
|
+
import './chunk-LJG6EMLJ.js';
|
|
27
|
+
import './chunk-E6CO3MJE.js';
|
|
28
|
+
import './chunk-VNVZDWO3.js';
|
|
29
|
+
import './chunk-XD4ZCMA4.js';
|
|
30
|
+
import './chunk-MESOOLF4.js';
|
|
31
|
+
import './chunk-Z5GEFMRW.js';
|
|
32
|
+
|
|
33
|
+
createRequire(import.meta.url);
|
package/dist/index.js
CHANGED
|
@@ -26,5 +26,5 @@ if (argv[0] === "claude") {
|
|
|
26
26
|
process.exit(await runClaudeLauncher(argv.slice(1)));
|
|
27
27
|
}
|
|
28
28
|
(await import('./hook-fast-FJ2PMBML.js')).recordLinkExitFailure(argv);
|
|
29
|
-
var { runCli } = await import('./cli-
|
|
29
|
+
var { runCli } = await import('./cli-OHQKHGEU.js');
|
|
30
30
|
await runCli(process.argv);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { criteriaFromAcceptanceFile, runEmitCriteria } from './chunk-J42MMS4R.js';
|
|
4
|
+
import './chunk-ESVW4MJY.js';
|
|
3
5
|
import { getJson, postJson, patchJson, WorkCommandError, resolveCredentials } from './chunk-6JNCNHSV.js';
|
|
4
6
|
import { autoConnect } from './chunk-C3XLVHN6.js';
|
|
5
7
|
import './chunk-B3EE7OKK.js';
|
|
@@ -19,6 +21,7 @@ import './chunk-G63KT5I5.js';
|
|
|
19
21
|
import './chunk-5BHIBJDA.js';
|
|
20
22
|
import './chunk-CDDED64E.js';
|
|
21
23
|
import './chunk-RA3O54FC.js';
|
|
24
|
+
import './chunk-T2Q7ISPE.js';
|
|
22
25
|
import './chunk-WZBHBH3J.js';
|
|
23
26
|
import './chunk-CRB3POP4.js';
|
|
24
27
|
import './chunk-LJG6EMLJ.js';
|
|
@@ -115,8 +118,59 @@ async function runSpecEdit(opts) {
|
|
|
115
118
|
payload,
|
|
116
119
|
opts.fetchImpl ?? defaultFetch
|
|
117
120
|
);
|
|
118
|
-
const
|
|
119
|
-
|
|
121
|
+
const criteria = await recordEditedCriteria(opts, creds, data, criteriaFromAcceptanceFile(body));
|
|
122
|
+
const text = [`\u2713 edited ${String(data.key)}`, ` ${String(data.url)}`, ...updatedAtLine(data), ...criteria.lines].join("\n");
|
|
123
|
+
return finish(opts, criteria.result === null ? data : { ...data, criteria: criteria.result }, text);
|
|
124
|
+
}
|
|
125
|
+
function sameList(read, held) {
|
|
126
|
+
return read.length === held.length && read.every((criterion, i) => criterion.id === held[i].id && criterion.text === held[i].text && criterion.proof === (held[i].proof ?? null));
|
|
127
|
+
}
|
|
128
|
+
async function recordEditedCriteria(opts, creds, edited, criteria) {
|
|
129
|
+
if (criteria.length === 0) return { result: null, lines: [] };
|
|
130
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
131
|
+
let item;
|
|
132
|
+
let held;
|
|
133
|
+
try {
|
|
134
|
+
const query = new URLSearchParams({ key: opts.key });
|
|
135
|
+
if (typeof edited.tracker === "string") query.set("tracker", edited.tracker);
|
|
136
|
+
const page = await getJson(creds.url, `/v1/work-items?${query}`, creds.apiKey, creds.appId, fetchImpl);
|
|
137
|
+
item = page.data[0];
|
|
138
|
+
if (item === void 0) return { result: null, lines: [] };
|
|
139
|
+
const read = await getJson(creds.url, `/v1/work-items/${item.id}`, creds.apiKey, creds.appId, fetchImpl);
|
|
140
|
+
held = read.data.criteria?.items;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return { result: null, lines: [`\u2717 the edit's criteria were not checked against the issue's work item: ${err.message}`] };
|
|
143
|
+
}
|
|
144
|
+
const count = criteria.length;
|
|
145
|
+
const counted = `${count} ${count === 1 ? "criterion" : "criteria"}`;
|
|
146
|
+
if (held !== void 0 && sameList(criteria, held)) {
|
|
147
|
+
return { result: { item: item.number, outcome: "unchanged", count }, lines: [` item #${item.number} already holds these ${counted}`] };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await runEmitCriteria({
|
|
151
|
+
file: `the body of ${opts.key}`,
|
|
152
|
+
list: { criteria },
|
|
153
|
+
item: String(item.number),
|
|
154
|
+
quiet: true,
|
|
155
|
+
url: creds.url,
|
|
156
|
+
apiKey: creds.apiKey,
|
|
157
|
+
appId: creds.appId,
|
|
158
|
+
cwd: opts.cwd,
|
|
159
|
+
home: opts.home,
|
|
160
|
+
env: opts.env,
|
|
161
|
+
fetchImpl
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const reason = err.message;
|
|
165
|
+
return {
|
|
166
|
+
result: { item: item.number, outcome: "not_recorded", count, reason },
|
|
167
|
+
lines: [
|
|
168
|
+
`\u2717 item #${item.number} is still held to its earlier criteria: ${reason}`,
|
|
169
|
+
` To record the new ones, save the body as a .md file and run: outerlayer emit criteria <file> --item ${item.number}`
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { result: { item: item.number, outcome: "recorded", count }, lines: [`\u2713 recorded ${counted} on item #${item.number}`] };
|
|
120
174
|
}
|
|
121
175
|
|
|
122
176
|
export { runSpecEdit, runSpecFile, runSpecSearch, runSpecTarget };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outerlayer/cli",
|
|
3
|
-
"version": "0.33.0-next.
|
|
3
|
+
"version": "0.33.0-next.20261022160949",
|
|
4
4
|
"description": "OuterLayer CLI — commands: init, claude, connect, doctor, login, logout, sync, daemon (alias: watch), emit, findings, work, spec, runner, hooks, mcp, context. Capture and sync coding-agent sessions, and record proof of factory work.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Magu Studios, Inc.",
|
|
@@ -258,14 +258,18 @@ Run them in order.
|
|
|
258
258
|
`"$(git rev-parse --absolute-git-dir)/build-reports/criteria.json"` and runs
|
|
259
259
|
`outerlayer emit criteria` on it. The gateway decides whether a list may be
|
|
260
260
|
recorded. A 409 means the item already has a recorded list. That list
|
|
261
|
-
stands, and nothing is recorded.
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
stands, and nothing is recorded. The script then reads the item's list with
|
|
262
|
+
`outerlayer work status --item <n> --json`. When it differs from the
|
|
263
|
+
issue's criteria, the script reports a step on the item that says so, and
|
|
264
|
+
the item's Activity tab shows it. The build holds the work to its own
|
|
265
|
+
reading of the criteria. The final report says which list the item holds
|
|
266
|
+
and how they differ. Any other refusal is written to the state
|
|
264
267
|
file with its message, and the build continues. The script writes the
|
|
265
268
|
outcome under `criteria` in the state file: `source` (`issue`, `drafted`,
|
|
266
269
|
`item`, or `none` when even a draft holds no criterion), `recorded`,
|
|
267
270
|
`count`, `list` and `numbered`. On a 409, `list` is empty, and the build's
|
|
268
|
-
reading of the criteria goes under `issueReading`.
|
|
271
|
+
reading of the criteria goes under `issueReading`. The item's own list goes
|
|
272
|
+
under `recordedList`, and `differs` says whether the two lists differ.
|
|
269
273
|
|
|
270
274
|
Drafted criteria are tiered, proven and judged like any others. A drafted
|
|
271
275
|
list stands like any recorded list. A later build's record
|
|
@@ -27,10 +27,12 @@ Judge only that group. Other judges handle the rest.
|
|
|
27
27
|
1. Read the criteria from the build's state file, under `criteria.list`. They
|
|
28
28
|
are the item's recorded list. If the state file lacks `criteria`, read the
|
|
29
29
|
`criteria` array in `criteria.json` in the build-reports directory. When
|
|
30
|
-
`criteria.source` is `item`, the item already had a recorded list
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
`criteria.source` is `item`, the item already had a recorded list, so
|
|
31
|
+
`list` is empty. Judge `criteria.recordedList` instead: it is the list the
|
|
32
|
+
item is held to. When `criteria.differs` is true, say in the report that
|
|
33
|
+
the issue states other criteria. When there is no `recordedList`, judge
|
|
34
|
+
`criteria.issueReading`, and say in the report that it is the issue's
|
|
35
|
+
reading, whose ids may differ from the item's recorded list. Each has an id
|
|
34
36
|
and a text, and the text is usually a Given, When and Then. Judge every criterion you were given,
|
|
35
37
|
including one no line of the diff touched. A criterion with `proof: null`
|
|
36
38
|
is proven by a citing test or any artifact bound to its id.
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
// `AC-<item>-NN` in order. A drafted list is always numbered and is recorded
|
|
14
14
|
// with `drafted: true`. An empty list records nothing. The script never edits
|
|
15
15
|
// the issue, and never replaces a list the item already has (the gateway
|
|
16
|
-
// answers 409 and the item's list stands).
|
|
16
|
+
// answers 409 and the item's list stands). When that list differs from the
|
|
17
|
+
// one read here, it reports a step on the item, so Work shows that the item
|
|
18
|
+
// is held to criteria the issue no longer states.
|
|
17
19
|
import { spawnSync } from "node:child_process";
|
|
18
20
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
21
|
import { dirname, join } from "node:path";
|
|
@@ -78,6 +80,17 @@ export function readCriteria(input, item, { drafted = false } = {}) {
|
|
|
78
80
|
return numbered ? { criteria, numbered, numberedBecause: problem } : { criteria, numbered };
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/** The step reported on the item when its recorded list differs from the issue's. At most 80 characters. */
|
|
84
|
+
export const DIFFERS_STEP = "Criteria differ from the issue: a person must replace them";
|
|
85
|
+
|
|
86
|
+
/** Whether two lists hold the same criteria, in the same order, with the same text and proof. */
|
|
87
|
+
export function sameCriteria(a, b) {
|
|
88
|
+
return (
|
|
89
|
+
a.length === b.length &&
|
|
90
|
+
a.every((criterion, n) => criterion.id === b[n].id && criterion.text === b[n].text && (criterion.proof ?? null) === (b[n].proof ?? null))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
/** Sorts a failed `emit criteria` into "the item already has a list" and any other refusal. */
|
|
82
95
|
export function classifyEmitFailure(message) {
|
|
83
96
|
// Only the CLI's replace refusal reads "refused (409):". Other answers that
|
|
@@ -85,16 +98,20 @@ export function classifyEmitFailure(message) {
|
|
|
85
98
|
return /\brefused \(409\):/.test(message) ? "item-list-stands" : "refused";
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
/** Runs `outerlayer`, or `npx -y @outerlayer/cli` on a machine with no `outerlayer` on PATH. */
|
|
102
|
+
function runCli(spawn, args) {
|
|
103
|
+
const options = { encoding: "utf8", timeout: 120000 };
|
|
104
|
+
const run = spawn("outerlayer", args, options);
|
|
105
|
+
return run.error?.code === "ENOENT" ? spawn("npx", ["-y", "@outerlayer/cli", ...args], options) : run;
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
/**
|
|
89
109
|
* Builds the `emit` function that runs `outerlayer emit criteria`; `spawn` is `spawnSync`, or a stand-in in tests.
|
|
90
110
|
* A machine that ran the CLI through `npx` has no `outerlayer` on PATH, so a missing command falls back to `npx -y @outerlayer/cli`.
|
|
91
111
|
*/
|
|
92
112
|
export function makeEmit(spawn, item) {
|
|
93
113
|
return (file) => {
|
|
94
|
-
const
|
|
95
|
-
const options = { encoding: "utf8", timeout: 120000 };
|
|
96
|
-
let run = spawn("outerlayer", args, options);
|
|
97
|
-
if (run.error?.code === "ENOENT") run = spawn("npx", ["-y", "@outerlayer/cli", ...args], options);
|
|
114
|
+
const run = runCli(spawn, ["emit", "criteria", file, "--item", item]);
|
|
98
115
|
if (run.status === 0) return { ok: true };
|
|
99
116
|
if (run.error) return { ok: false, message: `could not run outerlayer emit criteria: ${run.error.message}` };
|
|
100
117
|
if (run.signal) return { ok: false, message: `outerlayer emit criteria was stopped by ${run.signal}` };
|
|
@@ -102,6 +119,25 @@ export function makeEmit(spawn, item) {
|
|
|
102
119
|
};
|
|
103
120
|
}
|
|
104
121
|
|
|
122
|
+
/** Builds the `readRecorded` function: the item's recorded list, or `null` when it cannot be read. */
|
|
123
|
+
export function makeReadRecorded(spawn, item) {
|
|
124
|
+
return () => {
|
|
125
|
+
const run = runCli(spawn, ["work", "status", "--item", item, "--json"]);
|
|
126
|
+
if (run.status !== 0) return null;
|
|
127
|
+
try {
|
|
128
|
+
const items = JSON.parse(run.stdout).criteria?.items;
|
|
129
|
+
return Array.isArray(items) ? items.map((c) => ({ id: c.id, text: c.text, proof: c.proof ?? null })) : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Builds the `reportStep` function, which reports a step on the item. Returns whether it was recorded. */
|
|
137
|
+
export function makeReportStep(spawn, item) {
|
|
138
|
+
return (name) => runCli(spawn, ["emit", "step", name, "--item", item]).status === 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
105
141
|
function readState(path) {
|
|
106
142
|
try {
|
|
107
143
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -115,7 +151,7 @@ function readState(path) {
|
|
|
115
151
|
* the parsed criteria file. `emit(file)` runs `outerlayer emit criteria
|
|
116
152
|
* <file>` and returns `{ ok: true }` or `{ ok: false, message }`.
|
|
117
153
|
*/
|
|
118
|
-
export async function recordCriteria({ input, item, drafted = false, reportsDir, statePath, emit }) {
|
|
154
|
+
export async function recordCriteria({ input, item, drafted = false, reportsDir, statePath, emit, readRecorded, reportStep }) {
|
|
119
155
|
const read = readCriteria(input, item, { drafted });
|
|
120
156
|
const numbering = read.numbered ? { numbered: true, numberedBecause: read.numberedBecause } : { numbered: false };
|
|
121
157
|
let outcome;
|
|
@@ -132,13 +168,21 @@ export async function recordCriteria({ input, item, drafted = false, reportsDir,
|
|
|
132
168
|
if (result.ok) {
|
|
133
169
|
outcome = { source, recorded: true, ...base };
|
|
134
170
|
} else if (classifyEmitFailure(result.message) === "item-list-stands") {
|
|
135
|
-
//
|
|
171
|
+
// `list` stays empty and the build's reading goes under `issueReading`.
|
|
172
|
+
// The item's own list goes under `recordedList` when it could be read,
|
|
173
|
+
// and `differs` says whether it is another list (`null`: unknown).
|
|
174
|
+
const recorded = readRecorded === undefined ? null : await readRecorded();
|
|
175
|
+
const differs = recorded === null ? null : !sameCriteria(recorded, read.criteria);
|
|
176
|
+
const reported = differs === true && reportStep !== undefined ? await reportStep(DIFFERS_STEP) : false;
|
|
136
177
|
outcome = {
|
|
137
178
|
source: "item",
|
|
138
179
|
recorded: false,
|
|
139
180
|
count: read.criteria.length,
|
|
140
181
|
list: [],
|
|
141
182
|
issueReading: read.criteria,
|
|
183
|
+
...(recorded === null ? {} : { recordedList: recorded }),
|
|
184
|
+
differs,
|
|
185
|
+
...(differs === true ? { differsReported: reported } : {}),
|
|
142
186
|
...numbering,
|
|
143
187
|
note: "the item already has a recorded list; it stands",
|
|
144
188
|
};
|
|
@@ -156,7 +200,17 @@ export function summarize(outcome) {
|
|
|
156
200
|
const noun = n === 1 ? "criterion" : "criteria";
|
|
157
201
|
const ids = outcome.numbered ? `, numbered AC-<item>-NN because ${outcome.numberedBecause}` : ", with the issue's own ids";
|
|
158
202
|
if (outcome.source === "none") return "no criteria in the file; none recorded";
|
|
159
|
-
if (outcome.source === "item")
|
|
203
|
+
if (outcome.source === "item") {
|
|
204
|
+
if (outcome.differs === false) return `the item already has these ${n} ${noun} recorded`;
|
|
205
|
+
if (outcome.differs === true) {
|
|
206
|
+
const where = outcome.differsReported ? "; the item's Activity tab says so" : "; reporting it on the item failed, so say it in the final report";
|
|
207
|
+
return (
|
|
208
|
+
`the item's recorded list differs from the ${n} ${noun} read here. It stands, and the item is held to it${where}. ` +
|
|
209
|
+
"A person replaces it with `outerlayer emit criteria`"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return `the item already has a recorded list; it stands, and the ${n} ${noun} read here were not recorded`;
|
|
213
|
+
}
|
|
160
214
|
const what = outcome.source === "drafted" ? `drafted ${n} ${noun} from the issue's description` : `read ${n} ${noun} from the issue`;
|
|
161
215
|
return outcome.recorded
|
|
162
216
|
? `${what} and recorded them${outcome.source === "drafted" ? " as drafted" : ids}`
|
|
@@ -195,6 +249,8 @@ async function main(argv) {
|
|
|
195
249
|
reportsDir: join(gitDir, "build-reports"),
|
|
196
250
|
statePath: join(gitDir, "build-state.json"),
|
|
197
251
|
emit: makeEmit(spawnSync, item),
|
|
252
|
+
readRecorded: makeReadRecorded(spawnSync, item),
|
|
253
|
+
reportStep: makeReportStep(spawnSync, item),
|
|
198
254
|
});
|
|
199
255
|
process.stdout.write(`${summarize(outcome)}\n`);
|
|
200
256
|
return 0;
|