spec-ctx 0.4.1 → 0.6.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/README.md +124 -264
- package/dist/adopt/plan.js +40 -7
- package/dist/adopt/write.js +27 -4
- package/dist/assets.generated.js +1 -1
- package/dist/check/check.js +63 -23
- package/dist/cli/args.js +1 -0
- package/dist/cli/commands.js +45 -8
- package/dist/cli/gate.js +2 -1
- package/dist/cli/help.js +12 -5
- package/dist/cli/workflow.js +8 -3
- package/dist/cli.js +45 -6
- package/dist/graph/bundle.js +145 -18
- package/dist/graph/files.js +2 -1
- package/dist/graph/related.js +1 -1
- package/dist/graph/trust.js +114 -0
- package/dist/index/scan.js +2 -2
- package/dist/init/init.js +21 -11
- package/dist/init/next.js +50 -0
- package/dist/journal/journal.js +65 -0
- package/dist/journal/note.js +57 -0
- package/dist/journal/report.js +115 -0
- package/dist/journal/signals.js +26 -0
- package/dist/loop/fails.js +61 -10
- package/dist/loop/openspec.js +2 -1
- package/dist/main.js +16 -1
- package/dist/tasks/evidence.js +3 -3
- package/dist/util/env.js +10 -0
- package/dist/util/redact.js +57 -0
- package/dist/verify/run.js +2 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/specctx-run/SKILL.md +2 -2
package/dist/adopt/write.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { UsageExit } from "../cli/errors.js";
|
|
5
|
-
import { CONFIG_FILE } from "../config.js";
|
|
5
|
+
import { CONFIG_FILE, ConfigError } from "../config.js";
|
|
6
6
|
import { loadConfigOrExit } from "../cli/config.js";
|
|
7
7
|
import { runCommand } from "../runcmd.js";
|
|
8
8
|
import { dumps } from "../util/json.js";
|
|
@@ -44,6 +44,12 @@ export function reportText(plan) {
|
|
|
44
44
|
out.push("## Capability codes", "", "Rename a code in `spec-ctx.toml` before the IDs spread.", "", "| Code | Path | Source |", "| --- | --- | --- |");
|
|
45
45
|
for (const c of plan.caps)
|
|
46
46
|
out.push(`| ${c.code} | ${c.path} | ${c.derived ? "derived" : "existing"} |`);
|
|
47
|
+
// REQ-ADOPT-012 the families and their prefix (063/D5)
|
|
48
|
+
if (plan.families.length) {
|
|
49
|
+
out.push("", "Families of three or more paths with one first segment take a family code first. The prefix is the first three letters of that segment.", "");
|
|
50
|
+
for (const f of plan.families)
|
|
51
|
+
out.push(`- \`${f.segment}\`: prefix \`${f.prefix}\`, ${f.count} codes`);
|
|
52
|
+
}
|
|
47
53
|
out.push("", "## Headers", "", "| File | Line | Old header | New header |", "| --- | --- | --- | --- |");
|
|
48
54
|
const cell = (s) => "`" + s.replace(/\|/g, "\\|").replace(/`/g, "'") + "`";
|
|
49
55
|
for (const e of plan.edits)
|
|
@@ -58,6 +64,20 @@ export function reportText(plan) {
|
|
|
58
64
|
}
|
|
59
65
|
return out.join("\n") + "\n";
|
|
60
66
|
}
|
|
67
|
+
/** REQ-ADOPT-011 the command after adopt. The codes in spec-ctx.toml changed, so the config loads again. */
|
|
68
|
+
async function nextAfterAdopt(root, run) {
|
|
69
|
+
const { loadIndex } = await import("../index/build.js");
|
|
70
|
+
const { nextTask } = await import("../tasks/tasks.js");
|
|
71
|
+
try {
|
|
72
|
+
const task = nextTask(root, loadIndex(root, loadConfigOrExit(root)).data.entries);
|
|
73
|
+
return runCommand(run, task ? `context ${task.id}` : "status");
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof UsageExit || e instanceof ConfigError)
|
|
77
|
+
return runCommand(run, "status");
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
export async function applyAdopt(root, cfg, plan, io, json) {
|
|
62
82
|
const cfgPath = join(root, CONFIG_FILE);
|
|
63
83
|
const cfgBefore = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : null;
|
|
@@ -115,7 +135,7 @@ export async function applyAdopt(root, cfg, plan, io, json) {
|
|
|
115
135
|
const files = new Set(plan.edits.map((e) => e.file)).size;
|
|
116
136
|
const report = plan.edits.length ? REPORT_FILE : null;
|
|
117
137
|
if (json)
|
|
118
|
-
io.out(dumps({ adopted: plan.edits.length, caps: plan.caps, files, report, unmatched: plan.unmatched }) + "\n");
|
|
138
|
+
io.out(dumps({ adopted: plan.edits.length, caps: plan.caps, families: plan.families, files, report, unmatched: plan.unmatched }) + "\n");
|
|
119
139
|
else {
|
|
120
140
|
io.out(plan.unmatched.map((u) => `unmatched: ${u.file}:${u.line} ${u.header}\n`).join(""));
|
|
121
141
|
// REQ-CLI-007 only the codes in spec-ctx.toml changed: no header, no report
|
|
@@ -123,8 +143,11 @@ export async function applyAdopt(root, cfg, plan, io, json) {
|
|
|
123
143
|
io.out(`wrote the codes to ${CONFIG_FILE}\nnothing to adopt\n`);
|
|
124
144
|
else {
|
|
125
145
|
io.out(`adopted ${plan.edits.length} header(s) in ${files} file(s). Report: ${REPORT_FILE}\n`);
|
|
126
|
-
// REQ-ADOPT-002 adopt writes no baseline. The
|
|
127
|
-
|
|
146
|
+
// REQ-ADOPT-002 adopt writes no baseline. The gate step is a later, separate commit (044/D4, ADR-006 item 6).
|
|
147
|
+
// REQ-ADOPT-011 the last line names the context of the task that nextTask picks, or status
|
|
148
|
+
const run = cfg.cli.run;
|
|
149
|
+
io.out(`To gate the old debt later, run ${runCommand(run, "baseline write")} and commit it with a subject that starts with baseline:\n`);
|
|
150
|
+
io.out(`next: ${await nextAfterAdopt(root, run)}\n`);
|
|
128
151
|
}
|
|
129
152
|
}
|
|
130
153
|
return 0;
|
package/dist/assets.generated.js
CHANGED
|
@@ -10,5 +10,5 @@ export const ASSETS = {
|
|
|
10
10
|
"skills/specctx-bootstrap/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtYm9vdHN0cmFwCmRlc2NyaXB0aW9uOiBTdGFydCBhIHNlc3Npb24gaW4gYW4gT3BlblNwZWMgcHJvamVjdCB0aGF0IHVzZXMgc3BlYy1jdHguIFVzZSBhdCB0aGUgc3RhcnQgb2YgZXZlcnkgc2Vzc2lvbiwgYW5kIG9uICJzdGFydCIsICJib290c3RyYXAiLCBvciAid2hhdCBzaG91bGQgSSB3b3JrIG9uIi4gTG9hZHMgb25seSB0aGUgc3BlYyB0ZXh0IHRoYXQgdGhlIG5leHQgdGFzayBuZWVkcy4KLS0tCgpUaGlzIHNraWxsIGlzIG5vdyBgc3BlY2N0eC1ydW5gLiBSdW4gaXQuCg==",
|
|
11
11
|
"skills/specctx-done/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtZG9uZQpkZXNjcmlwdGlvbjogQ2xvc2UgYSB0YXNrIGluIGFuIE9wZW5TcGVjIHByb2plY3QgdGhhdCB1c2VzIHNwZWMtY3R4LiBSdW5zIHRoZSBWZXJpZnkgY29tbWFuZCwgd3JpdGVzIGV2aWRlbmNlLCBzZXRzIHRoZSBjaGVja2JveCwgd3JpdGVzIHRoZSBoYW5kb2ZmLCBhbmQgc2hvd3MgdGhlIG1ldHJpY3MuIFVzZSB3aGVuIHRoZSB3b3JrIG9mIGEgdGFzayBpcyBjb21wbGV0ZSwgb3Igb24gImRvbmUiLCAiY2xvc2UgdGhlIHRhc2siLCBhbmQgImVuZCB0aGUgc2Vzc2lvbiIuCi0tLQoKVGhpcyBza2lsbCBpcyBub3cgYHNwZWNjdHgtcnVuYC4gUnVuIGl0Lgo=",
|
|
12
12
|
"skills/specctx-next/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtbmV4dApkZXNjcmlwdGlvbjogUGljayBhbmQgc3RhcnQgdGhlIG5leHQgdGFzayBpbiB0aGUgbWlkZGxlIG9mIGEgc2Vzc2lvbiBpbiBhbiBPcGVuU3BlYyBwcm9qZWN0IHRoYXQgdXNlcyBzcGVjLWN0eC4gVXNlIGFmdGVyIHlvdSBjbG9zZSBhIHRhc2ssIG9yIG9uICJuZXh0IHRhc2siIGFuZCAid2hhdCBpcyBuZXh0Ii4KLS0tCgpUaGlzIHNraWxsIGlzIG5vdyBgc3BlY2N0eC1ydW5gLiBSdW4gaXQuCg==",
|
|
13
|
-
"skills/specctx-run/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtcnVuCmRlc2NyaXB0aW9uOiBSdW4gdGhlIHNwZWMtY3R4IGxvb3AgaW4gYW4gT3BlblNwZWMgcHJvamVjdC4gVXNlIG9uICJydW4iLCAiY29udGludWUiLCAibmV4dCB0YXNrIiwgImJ1aWxkIHRoaXMiLCAiY2Fycnkgb24iLCBhbmQgb24gYSBmaXJzdCBtZXNzYWdlIHRoYXQgZGVzY3JpYmVzIGEgY2hhbmdlIHRvIGJ1aWxkLiBUaGUgZW5naW5lIHBpY2tzIHRoZSBuZXh0IGFjdGlvbi4gVGhlIHNraWxsIGFjdHMgb24gaXQgdW50aWwgdGhlIGNoYW5nZSByZWFjaGVzIHRoZSBhcmNoaXZlIG9yIGEgaHVtYW4gbXVzdCBkZWNpZGUuCi0tLQoKIyBzcGVjY3R4LXJ1bgoKUnVuIGBzcGVjY3R4YCBhcyBgbnB4IHNwZWMtY3R4YCB3aGVuIGl0IGlzIG5vdCBpbnN0YWxsZWQuIGBvcGVuc3BlY2AgaXMgdGhlIGNvbW1hbmQgaW4gYFtnYXRlXSBvcGVuc3BlY2Agb2YgYHNwZWMtY3R4LnRvbWxgLiBXaGVuIHRoYXQga2V5IGlzIGFic2VudCwgYG9wZW5zcGVjYCBpcyBgbnB4IC15IEBmaXNzaW9uLWFpL29wZW5zcGVjQGxhdGVzdGAuCgojIyBUaGUgbG9vcAoKMS4gUnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYC4gUmVhZCBgYWN0aW9uYCwgYGNoYW5nZWAgKGA8Yz5gKSwgYHRhc2tgIChgPHQ+YCksIGBjb21tYW5kYCwgYHJlYXNvbmAsIGFuZCBgaHVtYW5gLgoyLiBBY3Qgb24gYGFjdGlvbmAgYXMgdGhlIHRhYmxlIGJlbG93IHNheXMuCjMuIFJ1biBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KNC4gUmVwZWF0IGZyb20gc3RlcCAxIHVudGlsIGBhY3Rpb25gIGlzIGBzdG9wYCBvciBgaHVtYW5gIGlzIHRydWUuCgojIyBBY3Rpb25zCgotIGBwbGFuYDogdGFrZSB0aGUgY2hhbmdlIG5hbWUgZnJvbSB0aGUgcmVxdWVzdCBvZiB0aGUgdXNlciBpbiBrZWJhYiBjYXNlLiBUaGF0IG5hbWUgaXMgYDxjPmAuIEZvbGxvdyB0aGUgdXBzdHJlYW0gYG9wZW5zcGVjLXByb3Bvc2VgIHNraWxsIGZvciBgPGM+YC4gUmVhZCBleGlzdGluZyByZXF1aXJlbWVudHMgd2l0aCBgc3BlY2N0eCBidW5kbGUgPGlkPmAgb3IgYHNwZWNjdHggcXVlcnlgLCBuZXZlciBgb3BlbnNwZWMgc2hvd2AuIFRha2UgZXZlcnkgSUQgZnJvbSBgc3BlY2N0eCBpZCBuZXcgUkVRIDxDQVA+
|
|
13
|
+
"skills/specctx-run/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtcnVuCmRlc2NyaXB0aW9uOiBSdW4gdGhlIHNwZWMtY3R4IGxvb3AgaW4gYW4gT3BlblNwZWMgcHJvamVjdC4gVXNlIG9uICJydW4iLCAiY29udGludWUiLCAibmV4dCB0YXNrIiwgImJ1aWxkIHRoaXMiLCAiY2Fycnkgb24iLCBhbmQgb24gYSBmaXJzdCBtZXNzYWdlIHRoYXQgZGVzY3JpYmVzIGEgY2hhbmdlIHRvIGJ1aWxkLiBUaGUgZW5naW5lIHBpY2tzIHRoZSBuZXh0IGFjdGlvbi4gVGhlIHNraWxsIGFjdHMgb24gaXQgdW50aWwgdGhlIGNoYW5nZSByZWFjaGVzIHRoZSBhcmNoaXZlIG9yIGEgaHVtYW4gbXVzdCBkZWNpZGUuCi0tLQoKIyBzcGVjY3R4LXJ1bgoKUnVuIGBzcGVjY3R4YCBhcyBgbnB4IHNwZWMtY3R4YCB3aGVuIGl0IGlzIG5vdCBpbnN0YWxsZWQuIGBvcGVuc3BlY2AgaXMgdGhlIGNvbW1hbmQgaW4gYFtnYXRlXSBvcGVuc3BlY2Agb2YgYHNwZWMtY3R4LnRvbWxgLiBXaGVuIHRoYXQga2V5IGlzIGFic2VudCwgYG9wZW5zcGVjYCBpcyBgbnB4IC15IEBmaXNzaW9uLWFpL29wZW5zcGVjQGxhdGVzdGAuCgojIyBUaGUgbG9vcAoKMS4gUnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYC4gUmVhZCBgYWN0aW9uYCwgYGNoYW5nZWAgKGA8Yz5gKSwgYHRhc2tgIChgPHQ+YCksIGBjb21tYW5kYCwgYHJlYXNvbmAsIGFuZCBgaHVtYW5gLgoyLiBBY3Qgb24gYGFjdGlvbmAgYXMgdGhlIHRhYmxlIGJlbG93IHNheXMuCjMuIFJ1biBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KNC4gUmVwZWF0IGZyb20gc3RlcCAxIHVudGlsIGBhY3Rpb25gIGlzIGBzdG9wYCBvciBgaHVtYW5gIGlzIHRydWUuCgojIyBBY3Rpb25zCgotIGBwbGFuYDogdGFrZSB0aGUgY2hhbmdlIG5hbWUgZnJvbSB0aGUgcmVxdWVzdCBvZiB0aGUgdXNlciBpbiBrZWJhYiBjYXNlLiBUaGF0IG5hbWUgaXMgYDxjPmAuIEZvbGxvdyB0aGUgdXBzdHJlYW0gYG9wZW5zcGVjLXByb3Bvc2VgIHNraWxsIGZvciBgPGM+YC4gUmVhZCBleGlzdGluZyByZXF1aXJlbWVudHMgd2l0aCBgc3BlY2N0eCBidW5kbGUgPGlkPmAgb3IgYHNwZWNjdHggcXVlcnlgLCBuZXZlciBgb3BlbnNwZWMgc2hvd2AuIFRha2UgZXZlcnkgSUQgZnJvbSBgc3BlY2N0eCBpZCBuZXcgUkVRIDxDQVA+YCBhbmQgYHNwZWNjdHggaWQgbmV3IFNDTiA8Q0FQPiAtLWZvciA8UkVRPmAuIGBpZCBuZXdgIHJlc2VydmVzIG5vdGhpbmcuIEEgc2Vjb25kIGNhbGwgYmVmb3JlIGBzcGVjY3R4IGJ1aWxkYCByZXR1cm5zIHRoZSBzYW1lIElELiBXaGVuIHRoZSBjaGFuZ2UgbmVlZHMgc2V2ZXJhbCBJRHMgb2Ygb25lIGtpbmQsIHJ1biBgc3BlY2N0eCBpZCBuZXcgLS1jb3VudCA8bj5gIG9uY2UuIEdpdmUgZXZlcnkgdGFzayBhIGBbUkVRLS4uLl1gIGNpdGF0aW9uIGFuZCBhIGBWZXJpZnk6YCBjb2RlIHNwYW4uIFRoZW4gcnVuIGBzcGVjY3R4IGNoZWNrIC0tc3RyaWN0YCBhbmQgYHNwZWNjdHggZ2F0ZSAtLXN0cmljdGAuIFRoZSBnYXRlIHJ1bnMgdGhlIE9wZW5TcGVjIHZhbGlkYXRlIGFuZCB0aGUgc3BlYy1sZW5ndGggY2hlY2suIEZpeCBldmVyeSBmaW5kaW5nLiBUaGVuIHJ1biBgc3BlY2N0eCBuZXh0IC0tanNvbmAgYW5kIHJ1biB0aGUgY29tbWFuZCBpdCBwcmludHMuIFRoaXMgaXMgdGhlIHBsYW4gc3RvcC4gYG5leHRgIHJldHVybnMgYGFza2Agd2l0aCBgaHVtYW46IHRydWVgIHVudGlsIHRoZSBodW1hbiBhcHByb3ZlcyB0aGUgcXVlc3Rpb24uIFlvdSBuZXZlciByZXNvbHZlIGEgcGxhbiBxdWVzdGlvbiB5b3Vyc2VsZi4KLSBgY2l0ZWA6IHJ1biBgc3BlY2N0eCBxdWVyeSAtLWNoYW5nZSA8Yz4gLS1raW5kIHJlcWAuIEFkZCB0aGUgYFtSRVEtLi4uXWAgY2l0YXRpb24gYW5kIGEgYFZlcmlmeTpgIGNvZGUgc3BhbiB0byB0aGUgdGFzayBsaW5lIG5hbWVkIGluIGB0YXNrYC4gUnVuIGBzcGVjY3R4IGNoZWNrYC4gQ29udGludWUuCi0gYHN0YXJ0YDogcnVuIGBzcGVjY3R4IHRhc2sgc3RhcnQgPHQ+YC4gUmVhZCB0aGUgYnVuZGxlIGl0IHByaW50cy4gRWFjaCBjYXJkIGVuZHMgd2l0aCB0cnVzdCBsYWJlbHMuIEFjdCBvbiBhIGxpbmUgbWFya2VkIGB2ZXJpZmllZGAgb3IgYGZyZXNoYC4gUmVyZWFkIGEgbGluZSBtYXJrZWQgYHN0YWxlIHNpbmNlYCwgYHVudmVyaWZpZWRgLCBvciBgbm8gZXZpZGVuY2VgIHdpdGggYHNwZWNjdHggYnVuZGxlIDxJRD5gIGJlZm9yZSB5b3UgYnVpbGQgb24gaXQuIENvbnRpbnVlLgotIGB3b3JrYDogaW1wbGVtZW50IHRoZSB0YXNrLiBUYWcgY29kZSB3aXRoIGAvLyBSRVEtLi4uYC4gVGFnIHRlc3RzIHdpdGggYC8vIENvdmVyczogUkVRLS4uLmAuIFRoZW4gcnVuIGBzcGVjY3R4IHRhc2sgZG9uZSA8dD5gLgotIGBmaXhgOiByZWFkIGByZWFzb25gLiBJdCBob2xkcyB0aGUgdGFpbCBvZiB0aGUgZmFpbGVkIFZlcmlmeSBvdXRwdXQuIEZpeCB0aGUgY2F1c2UuIFJ1biBgc3BlY2N0eCB0YXNrIGRvbmUgPHQ+YCBhZ2Fpbi4KLSBgcmVwYWlyYDogcnVuIHRoZSBjb21tYW5kIGluIHRoZSBgY29tbWFuZGAgZmllbGQuIEl0IHJ1bnMgVmVyaWZ5IGFnYWluIGZvciBhIHRpY2tlZCB0YXNrIHdob3NlIGV2aWRlbmNlIGlzIGFic2VudCBvciBzdGFsZS4KLSBgZ2F0ZWA6IHJ1biBgc3BlY2N0eCBnYXRlIC0tc3RyaWN0YC4gRml4IGV2ZXJ5IGZpbmRpbmcuIEZvciBhIGBzdGFsZS1ldmlkZW5jZWAgZmluZGluZywgZ28gYmFjayB0byBzdGVwIDEuIGBuZXh0YCByZXR1cm5zIGByZXBhaXJgIGFuZCBpdHMgY29tbWFuZCByZXBhaXJzIGV2ZXJ5IHN0YWxlIHRhc2suIFJ1biB0aGUgZ2F0ZSBhZ2FpbiB1bnRpbCBpdCBleGl0cyAwLgotIGBhcmNoaXZlYDogd2hlbiBgaHVtYW5gIGlzIHRydWUsIHJ1biBgc3BlY2N0eCBoYW5kb2ZmIGFzayAtLWNoYW5nZSA8Yz4gImFyY2hpdmUgPGM+PyJgIGFuZCBzdG9wLiBUaGlzIGlzIHRoZSBhcmNoaXZlIHN0b3AuIGBuZXh0YCByZXR1cm5zIGBhc2tgIHVudGlsIHRoZSBodW1hbiBhcHByb3ZlcyB0aGUgcXVlc3Rpb24uIFlvdSBuZXZlciByZXNvbHZlIGFuIGFyY2hpdmUgcXVlc3Rpb24geW91cnNlbGYuIEFmdGVyIGEgcmVqZWN0aW9uLCBgbmV4dGAgcmV0dXJucyBgYXJjaGl2ZWAgd2l0aCBgaHVtYW46IHRydWVgIGFnYWluLiBXaGVuIGBodW1hbmAgaXMgZmFsc2UsIHRoZSB1c2VyIGFwcHJvdmVkIG9yIGBbbG9vcF0gYXJjaGl2ZWAgaXMgYGF1dG9gLiBUaGVuIHJ1biBlYWNoIHN0ZXAgdW5kZXIgYG9wZXJhdGlvbnMuYXJjaGl2ZS5ndWlkYW5jZWAgaW4gYG9wZW5zcGVjL2NvbmZpZy55YW1sYC4gVGhlbiBydW4gdGhlIGNvbW1hbmQgaW4gdGhlIGBjb21tYW5kYCBmaWVsZC4gVGhlbiBydW4gYG9wZW5zcGVjIHZhbGlkYXRlIC0tYWxsIC0tc3RyaWN0YCwgYHNwZWNjdHggY2hlY2sgLS1zdHJpY3RgLCBhbmQgYHNwZWNjdHggaGFuZG9mZiB3cml0ZWAuCi0gYGFza2A6IHByaW50IGByZWFzb25gLiBJdCBpcyB0aGUgb3BlbiBxdWVzdGlvbi4gU3RvcC4gWW91IG5ldmVyIHJlc29sdmUgYSBwbGFuIG9yIGFyY2hpdmUgcXVlc3Rpb24geW91cnNlbGYuIFRoZSBodW1hbiBkb2VzLCB3aXRoIGBzcGVjY3R4IGhhbmRvZmYgcmVzb2x2ZSA8bj4gLS1hcHByb3ZlICI8YW5zd2VyPiJgIG9yIGAtLXJlamVjdCAiPGFuc3dlcj4iYC4gQSByZWplY3Rpb24ga2VlcHMgdGhlIHN0b3AuIEFueSBvdGhlciBxdWVzdGlvbiB0YWtlcyBgc3BlY2N0eCBoYW5kb2ZmIHJlc29sdmUgPG4+ICI8YW5zd2VyPiJgLgotIGBzdG9wYDogcHJpbnQgYHJlYXNvbmAuIFN0b3AuCgojIyBSdWxlcwoKLSBUaGlzIHNraWxsIG92ZXJyaWRlcyB0d28gc3RlcHMgb2YgdGhlIHVwc3RyZWFtIGBvcGVuc3BlYy1wcm9wb3NlYCBza2lsbC4gSXQgcmVhZHMgc3BlY3Mgd2l0aCBgc3BlY2N0eCBidW5kbGVgIG9yIGBzcGVjY3R4IHF1ZXJ5YCwgbm90IGBvcGVuc3BlYyBzaG93YC4gSXQgbmV2ZXIgcnVucyB0aGUgY2xvc2luZyBgL29wc3g6YXBwbHlgLgotIE5ldmVyIHRpY2sgYSBjaGVja2JveCBieSBoYW5kLiBgc3BlY2N0eCB0YXNrIGRvbmVgIGlzIHRoZSBvbmx5IHdheSBhIHRhc2sgY2xvc2VzLgotIE5ldmVyIHVzZSBgLS1tYW51YWxgIG9uIGEgdGFzayB3aXRoIGEgVmVyaWZ5IGNvbW1hbmQuCi0gTmV2ZXIgY2FsbCBgL29wc3g6YXBwbHlgLiBUaGlzIHNraWxsIHJlcGxhY2VzIGl0LgotIFRvIHdyaXRlIGEgTU9ESUZJRUQgYmxvY2ssIHJ1biBgc3BlY2N0eCBidW5kbGUgPFJFUT4gLS1ibG9ja2AuIEl0IHByaW50cyB0aGUgYmxvY2sgYXMgaXQgc2l0cyBpbiB0aGUgc3BlYyBmaWxlLiBBIE1PRElGSUVEIGJsb2NrIGtlZXBzIGV2ZXJ5IHNjZW5hcmlvIHRpdGxlIHVuY2hhbmdlZC4KLSBgZ2F0ZWAgY291bnRzIHRoZSBSZWZzIGxpbmUgaW4gdGhlIDUwMCBjaGFyYWN0ZXJzIG9mIGEgcmVxdWlyZW1lbnQuCi0gVGhlIENIQU5HRUxPRyBlbnRyeSBnb2VzIHVuZGVyIGAjIyBVbnJlbGVhc2VkYCwgbmV2ZXIgdW5kZXIgYSByZWxlYXNlZCB2ZXJzaW9uLgotIEFmdGVyIGFuIGVkaXQgdW5kZXIgYHNraWxscy9gLCBydW4gYGJ1biBydW4gYnVpbGQgJiYgbm9kZSBkaXN0L2NsaS5qcyBpbml0IC0tc2tpbGxzIC0tZm9yY2VgIHRvIHJlZnJlc2ggdGhlIGNvcGllcy4KLSBOZXZlciByZWFkIGBvcGVuc3BlYy9zcGVjcy9gIG9yIGBvcGVuc3BlYy9jaGFuZ2VzLyovc3BlY3MvYCBkaXJlY3RseS4gVXNlIHRoZSBidW5kbGUgZnJvbSBgdGFzayBzdGFydGAgb3IgYHNwZWNjdHggYnVuZGxlIDxpZD5gLgotIFN0b3Agb24gYW55IGBhY3Rpb25gIG5vdCBpbiB0aGUgbGlzdCBhYm92ZS4gUHJpbnQgdGhlIG9iamVjdCBhbmQgYXNrIHRoZSB1c2VyLgotIEtlZXAgbm8gc3RhdGUgaW4geW91ciBjb250ZXh0LiBBIG5ldyBzZXNzaW9uIHN0YXJ0cyB0aGUgc2FtZSBsb29wIGFnYWluIHdpdGggc3RlcCAxLgo=",
|
|
14
14
|
};
|
package/dist/check/check.js
CHANGED
|
@@ -122,6 +122,65 @@ export function weakVerifyFindings(entries) {
|
|
|
122
122
|
export function weakVerifyWarnings(entries) {
|
|
123
123
|
return weakVerifyFindings(entries).map((f) => `weak-verify ${f.id} ${f.file}:${f.line}`);
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* REQ-CHK-010 REQ-CHK-008 REQ-CHK-009 one predicate for the delta test rule and the deferred notice (052/D1).
|
|
127
|
+
* A gap is a delta requirement or scenario of an active change that no test names. The rule reports the gaps of
|
|
128
|
+
* a change that has a task and no open task. The notice lists the gaps that an open task or a missing task hides.
|
|
129
|
+
*/
|
|
130
|
+
export function deltaGaps(entries, data) {
|
|
131
|
+
const covered = new Set(entries.filter((e) => e.kind === "test").flatMap((e) => e.refs));
|
|
132
|
+
const named = new Set(data.scn_named ?? []);
|
|
133
|
+
// an ID that a REMOVED block of an active change names waits for archive, so it needs no test
|
|
134
|
+
const pending = new Set();
|
|
135
|
+
for (const e of entries) {
|
|
136
|
+
if (e.status !== "removed")
|
|
137
|
+
continue;
|
|
138
|
+
pending.add(e.id);
|
|
139
|
+
for (const sc of e.scenarios ?? [])
|
|
140
|
+
pending.add(sc);
|
|
141
|
+
}
|
|
142
|
+
const out = new Map();
|
|
143
|
+
const slot = (change) => {
|
|
144
|
+
let g = out.get(change);
|
|
145
|
+
if (!g)
|
|
146
|
+
out.set(change, (g = { open: 0, all: 0, gaps: [] }));
|
|
147
|
+
return g;
|
|
148
|
+
};
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
if (e.kind !== "task" || e.change === "main" || e.file.includes("/archive/"))
|
|
151
|
+
continue;
|
|
152
|
+
const g = slot(e.change);
|
|
153
|
+
g.all++;
|
|
154
|
+
if (e.status !== "done")
|
|
155
|
+
g.open++;
|
|
156
|
+
}
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (const e of entries) {
|
|
159
|
+
if ((e.kind !== "scn" && e.kind !== "req") || e.change === "main" || (e.status !== "active" && e.status !== "modified"))
|
|
160
|
+
continue;
|
|
161
|
+
if (pending.has(e.id))
|
|
162
|
+
continue;
|
|
163
|
+
const rule = e.kind === "req" ? (covered.has(e.id) ? null : "uncovered") : named.has(e.id) ? null : "scenario-untested";
|
|
164
|
+
if (!rule)
|
|
165
|
+
continue;
|
|
166
|
+
const gap = [e.file, e.line, rule, e.id];
|
|
167
|
+
const key = e.change + JSON.stringify(gap);
|
|
168
|
+
if (seen.has(key))
|
|
169
|
+
continue;
|
|
170
|
+
seen.add(key);
|
|
171
|
+
slot(e.change).gaps.push(gap);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
export function deferredOf(entries, data) {
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const [change, g] of deltaGaps(entries, data)) {
|
|
178
|
+
// 052/D1 to include a change that has no task, the condition is `g.all === 0 || g.open > 0`
|
|
179
|
+
if ((g.all === 0 || g.open > 0) && g.gaps.length)
|
|
180
|
+
out.push({ change, untested: g.gaps.length, open: g.open });
|
|
181
|
+
}
|
|
182
|
+
return out.sort((x, y) => cmpTuple([x.change], [y.change]));
|
|
183
|
+
}
|
|
125
184
|
/** Sorted, unique findings. The scan never reads .ctx/, so evidence adds no index entry. */
|
|
126
185
|
export function check(root, cfg, data, strict) {
|
|
127
186
|
const entries = data.entries;
|
|
@@ -190,29 +249,10 @@ export function check(root, cfg, data, strict) {
|
|
|
190
249
|
if (e.kind === "scn" && !named.has(e.id))
|
|
191
250
|
findings.push([e.file, e.line, "scenario-untested", e.id]);
|
|
192
251
|
}
|
|
193
|
-
// REQ-CHK-008 a delta
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
continue;
|
|
198
|
-
const t = tasksOf.get(e.change) ?? { open: 0, all: 0 };
|
|
199
|
-
t.all++;
|
|
200
|
-
if (e.status !== "done")
|
|
201
|
-
t.open++;
|
|
202
|
-
tasksOf.set(e.change, t);
|
|
203
|
-
}
|
|
204
|
-
for (const e of entries) {
|
|
205
|
-
if ((e.kind !== "scn" && e.kind !== "req") || e.change === "main" || (e.status !== "active" && e.status !== "modified"))
|
|
206
|
-
continue;
|
|
207
|
-
const t = tasksOf.get(e.change);
|
|
208
|
-
if (!t || t.open > 0 || pending.has(e.id))
|
|
209
|
-
continue;
|
|
210
|
-
// REQ-CHK-009 a delta requirement needs a Covers tag on the same terms (039/D2)
|
|
211
|
-
if (e.kind === "req" && !covered.has(e.id))
|
|
212
|
-
findings.push([e.file, e.line, "uncovered", e.id]);
|
|
213
|
-
if (e.kind === "scn" && !named.has(e.id))
|
|
214
|
-
findings.push([e.file, e.line, "scenario-untested", e.id]);
|
|
215
|
-
}
|
|
252
|
+
// REQ-CHK-008 REQ-CHK-009 a delta item needs a test once its change has a task and no open task (038/D1, 038/D2, 039/D2)
|
|
253
|
+
for (const g of deltaGaps(entries, data).values())
|
|
254
|
+
if (g.open === 0 && g.all > 0)
|
|
255
|
+
findings.push(...g.gaps);
|
|
216
256
|
findings.push(...evidenceFindings(root, entries, listEvidence(root)));
|
|
217
257
|
}
|
|
218
258
|
const uniq = new Map();
|
package/dist/cli/args.js
CHANGED
|
@@ -10,6 +10,7 @@ export const COMMAND_FLAGS = {
|
|
|
10
10
|
bundle: { budget: "int", depth: "int", all: "bool", block: "bool" },
|
|
11
11
|
context: { budget: "int", depth: "int" },
|
|
12
12
|
check: { strict: "bool" },
|
|
13
|
+
report: {},
|
|
13
14
|
stats: { change: "string", write: "bool" },
|
|
14
15
|
gate: { strict: "bool", markdown: "string", rerun: "bool", base: "string" },
|
|
15
16
|
"id new": { for: "string", count: "int" },
|
package/dist/cli/commands.js
CHANGED
|
@@ -6,6 +6,7 @@ import { defaults } from "../config.js";
|
|
|
6
6
|
import { hintOf } from "../check/hints.js";
|
|
7
7
|
import { paint } from "./color.js";
|
|
8
8
|
import { runCommand } from "../runcmd.js";
|
|
9
|
+
import { note } from "../journal/note.js";
|
|
9
10
|
/** check with warnings and the baseline filter. Strict mode adds the strict rules and the warnings. */
|
|
10
11
|
async function runCheck(root, cfg, data, strict) {
|
|
11
12
|
const { check } = await import("../check/check.js");
|
|
@@ -28,12 +29,13 @@ async function runCheck(root, cfg, data, strict) {
|
|
|
28
29
|
* REQ-CHK-004 the findings, then a blank line, the summary line, and one line per rule with its hint.
|
|
29
30
|
* A warning counts as advisory. The `baselined` line stays last (REQ-CHK-002).
|
|
30
31
|
*/
|
|
31
|
-
export function checkText(r, tty, run = "specctx") {
|
|
32
|
+
export function checkText(r, tty, run = "specctx", deferred = []) {
|
|
32
33
|
// REQ-CLI-008 every finding of check is hard, so every finding line is red. stale-evidence is one of them.
|
|
33
34
|
// REQ-CHK-001 a finding with a detail, such as a retitle, carries it after the ID (021/D2)
|
|
34
35
|
const body = r.findings.map((x) => paint("fail", `${x.file}:${x.line} ${x.rule} ${x.id}${x.detail ? ` ${x.detail}` : ""}\n`, tty)).join("")
|
|
35
36
|
+ r.warnings.map((w) => `warning: ${w}\n`).join("");
|
|
36
|
-
|
|
37
|
+
// REQ-CHK-010 a deferred line is not a finding. It comes before the baselined line, which stays last (052/D3).
|
|
38
|
+
const base = deferred.map((d) => `deferred: ${d.change} ${d.untested} untested delta item${d.untested === 1 ? "" : "s"}, ${d.open} open task${d.open === 1 ? "" : "s"}\n`).join("") + (r.baselined ? `baselined: ${r.baselined}\n` : "");
|
|
37
39
|
const n = r.findings.length + r.warnings.length;
|
|
38
40
|
if (!n)
|
|
39
41
|
return paint("dim", "0 findings\n", tty) + base;
|
|
@@ -75,8 +77,24 @@ export async function run(p, io) {
|
|
|
75
77
|
return init(p, io);
|
|
76
78
|
const root = findRoot(io.cwd, f.root);
|
|
77
79
|
requireOpenSpec(root);
|
|
80
|
+
// REQ-JRN-001 the root is known now, so the bin entry may write the journal
|
|
81
|
+
note(io, { root });
|
|
82
|
+
// REQ-JRN-004 report runs before the config load, so a broken config still reports
|
|
83
|
+
if (cmd === "report") {
|
|
84
|
+
if (p.positional.length)
|
|
85
|
+
throw new UsageExit("report takes no arguments");
|
|
86
|
+
// REQ-JRN-008 report prints Markdown only
|
|
87
|
+
if (json)
|
|
88
|
+
throw new UsageExit("report takes no --json flag");
|
|
89
|
+
const { buildReport } = await import("../journal/report.js");
|
|
90
|
+
const { VERSION } = await import("../version.js");
|
|
91
|
+
io.out(buildReport(root, new Date(), { version: VERSION, node: process.version, platform: process.platform, arch: process.arch }).text);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
78
94
|
const { loadConfigOrExit } = await import("./config.js");
|
|
79
95
|
const cfg = loadConfigOrExit(root);
|
|
96
|
+
// REQ-JRN-006 the exit 3 line names the report command with this prefix
|
|
97
|
+
note(io, { run: cfg.cli.run });
|
|
80
98
|
const { dumps } = await import("../util/json.js");
|
|
81
99
|
const print = (v) => io.out(dumps(v) + "\n");
|
|
82
100
|
if (cmd === "build") {
|
|
@@ -287,6 +305,9 @@ export async function run(p, io) {
|
|
|
287
305
|
const { bundle } = await import("../graph/bundle.js");
|
|
288
306
|
const { resolveInputs } = await import("../graph/input.js");
|
|
289
307
|
const ids = canon(entries);
|
|
308
|
+
// REQ-QRY-018 a negative budget is a usage error (050/D11)
|
|
309
|
+
if (f.budget !== undefined && f.budget < 0)
|
|
310
|
+
throw new UsageExit("--budget must be 0 or more");
|
|
290
311
|
// REQ-QRY-005 an input that is no known ID is a path in the repository
|
|
291
312
|
const { wanted, paths, errors, taskId } = resolveInputs(root, io.cwd, entries, p.positional);
|
|
292
313
|
if (errors.length) {
|
|
@@ -302,8 +323,11 @@ export async function run(p, io) {
|
|
|
302
323
|
}
|
|
303
324
|
const { reqBlocks } = await import("../graph/bundle.js");
|
|
304
325
|
const text = reqBlocks(root, p.positional.map((i) => ids.get(i)));
|
|
326
|
+
// REQ-QRY-019 block mode has no budget, so budget is null and the text is complete (050/D6)
|
|
327
|
+
// REQ-QRY-026 block mode has no budget, so used equals chars and no block is listed
|
|
328
|
+
const n = (await import("../util/pystr.js")).cpLen(text);
|
|
305
329
|
if (json)
|
|
306
|
-
print({ text, omitted: [] });
|
|
330
|
+
print({ text, omitted: [], body_omitted: [], chars: n, budget: null, complete: true, used: n, blocks: [] });
|
|
307
331
|
else
|
|
308
332
|
io.out(text);
|
|
309
333
|
return 0;
|
|
@@ -311,6 +335,12 @@ export async function run(p, io) {
|
|
|
311
335
|
// 047/D5 a task input passes the entry that findTask picks, so an active task wins over an archived one
|
|
312
336
|
const task = taskId === undefined ? undefined : (await import("../tasks/tasks.js")).findTask(root, entries, taskId).entry;
|
|
313
337
|
const b = bundle(root, entries, wanted, f.budget ?? cfg.bundle.budget, f.depth ?? cfg.bundle.depth, { all, task });
|
|
338
|
+
// REQ-JRN-002 only context emits the block signals. The sink is set only for the bin entry.
|
|
339
|
+
if (cmd === "context" && io.journal) {
|
|
340
|
+
const { contextSignals } = await import("../journal/signals.js");
|
|
341
|
+
for (const signal of contextSignals(entries, b))
|
|
342
|
+
note(io, { signal });
|
|
343
|
+
}
|
|
314
344
|
if (json)
|
|
315
345
|
print(b);
|
|
316
346
|
else if (b.text === "" && wanted.every((w) => paths.has(w)))
|
|
@@ -343,10 +373,12 @@ export async function run(p, io) {
|
|
|
343
373
|
const r = await runCheck(root, cfg, data, !!f.strict);
|
|
344
374
|
// REQ-CHK-004 REQ-CHK-006 the JSON keeps its shape. A finding hint is for the human form only (031/D6).
|
|
345
375
|
const findings = r.findings.map(({ hint: _hint, ...rest }) => rest);
|
|
376
|
+
// REQ-CHK-010 only this case computes the deferred notice. runCheck and gate stay as they are (052/D5).
|
|
377
|
+
const deferred = f.strict ? (await import("../check/check.js")).deferredOf(entries, data) : [];
|
|
346
378
|
if (json)
|
|
347
|
-
print({ baselined: r.baselined, count: findings.length, findings, warnings: r.warnings });
|
|
379
|
+
print({ baselined: r.baselined, count: findings.length, findings, warnings: r.warnings, ...(f.strict ? { deferred } : {}) });
|
|
348
380
|
else
|
|
349
|
-
io.out(checkText(r, io.tty, cfg.cli.run));
|
|
381
|
+
io.out(checkText(r, io.tty, cfg.cli.run, deferred));
|
|
350
382
|
return r.findings.length ? 1 : 0;
|
|
351
383
|
}
|
|
352
384
|
}
|
|
@@ -369,18 +401,23 @@ async function init(p, io) {
|
|
|
369
401
|
return defaults().cli.run;
|
|
370
402
|
}
|
|
371
403
|
})();
|
|
404
|
+
// REQ-DIST-017 the next command comes from the state of the specs
|
|
405
|
+
const { detectNext } = await import("../init/next.js");
|
|
406
|
+
const d = detectNext(root, run);
|
|
407
|
+
const summary = d.summary ? `${d.summary}\n` : "";
|
|
372
408
|
if (p.flags.json)
|
|
373
|
-
io.out(JSON.stringify(r, null, 2) + "\n");
|
|
409
|
+
io.out(JSON.stringify({ ...r, needs_id: d.needs_id, next: d.next }, null, 2) + "\n");
|
|
374
410
|
else if (r.nothing)
|
|
375
|
-
io.out("nothing to do\n" + r.notes.map((n) => `${n}\n`).join("") + paint("dim", `next: ${
|
|
411
|
+
io.out("nothing to do\n" + r.notes.map((n) => `${n}\n`).join("") + summary + paint("dim", `next: ${d.next}\n`, io.tty));
|
|
376
412
|
else {
|
|
377
413
|
io.out(r.written.map((w) => `wrote ${w}\n`).join(""));
|
|
378
414
|
io.out(r.merged.map((m) => `merged ${m}\n`).join(""));
|
|
379
415
|
io.out(r.kept.map((k) => `kept ${k}\n`).join(""));
|
|
380
416
|
io.out(r.notes.map((n) => `${n}\n`).join(""));
|
|
417
|
+
io.out(summary);
|
|
381
418
|
// REQ-CLI-007 init ends with the next step. REQ-DIST-002 a kept changed file points to --force (014/D2).
|
|
382
419
|
const again = [runCommand(run, "init"), all ? "--all" : [p.flags.schema ? "--schema" : "", p.flags.skills ? "--skills" : ""].filter(Boolean).join(" "), "--force"].filter(Boolean).join(" ");
|
|
383
|
-
io.out(paint("dim", `next: ${r.changed.length ? again :
|
|
420
|
+
io.out(paint("dim", `next: ${r.changed.length ? again : d.next}\n`, io.tty));
|
|
384
421
|
}
|
|
385
422
|
return 0;
|
|
386
423
|
}
|
package/dist/cli/gate.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { join, resolve, sep } from "node:path";
|
|
5
|
+
import { childEnv } from "../util/env.js";
|
|
5
6
|
import { anyMatch, globRe } from "../util/glob.js";
|
|
6
7
|
import { cmpStr } from "../util/pystr.js";
|
|
7
8
|
const SKIP = new Set(["node_modules", ".git", ".ctx", "dist", "build"]);
|
|
@@ -93,7 +94,7 @@ async function gateSteps(root, o, io, ran) {
|
|
|
93
94
|
io.out(`linters: ok (${files} file(s)${baselined ? `, ${baselined} baselined` : ""})\n`);
|
|
94
95
|
ran.push("validate");
|
|
95
96
|
// gate 4: openspec validate with the command the caller resolved (018/D2)
|
|
96
|
-
const v = spawnSync("/bin/sh", ["-c", `${o.openspec} validate --all${o.strict ? " --strict" : ""} --no-interactive`], { cwd: root, encoding: "utf8" });
|
|
97
|
+
const v = spawnSync("/bin/sh", ["-c", `${o.openspec} validate --all${o.strict ? " --strict" : ""} --no-interactive`], { cwd: root, encoding: "utf8", env: childEnv() /* REQ-DIST-016 */ });
|
|
97
98
|
if (v.status !== 0) {
|
|
98
99
|
io.out("validate: fail\n" + (v.stdout ?? "") + (v.stderr ?? ""));
|
|
99
100
|
return "validate";
|
package/dist/cli/help.js
CHANGED
|
@@ -40,7 +40,7 @@ export const TABLE = [
|
|
|
40
40
|
{
|
|
41
41
|
name: "context", group: "Read", args: "<task|ID|path|change>", summary: "Print what an agent needs before it edits a task, an ID, a file, or a change.",
|
|
42
42
|
flags: [
|
|
43
|
-
["--budget <n>", "
|
|
43
|
+
["--budget <n>", "Keep the blocks to n characters. Each entry prints its card first. A card ends with its trust labels: the provenance and the evidence state. A card can end with | body omitted. Run bundle with its ID to get the body. A requested block always prints, also when it is longer. Then the over budget line gives the size. The truncated line names the entries that have no card. It is not in this limit. The default is 8000."],
|
|
44
44
|
["--depth <n>", "Follow links n steps. The default is 1."],
|
|
45
45
|
],
|
|
46
46
|
exit: [OK, ["2", "A usage error, an unknown ID, or a bad path. It takes one input. Use `bundle` for several inputs or for `--block`."], INTERNAL], example: "specctx context 045/2.1",
|
|
@@ -53,7 +53,7 @@ export const TABLE = [
|
|
|
53
53
|
{
|
|
54
54
|
name: "bundle", group: "Read", args: "<ID|path|change...>", summary: "Print the spec text for the IDs, files, or changes and their links.",
|
|
55
55
|
flags: [
|
|
56
|
-
["--budget <n>", "
|
|
56
|
+
["--budget <n>", "Keep the blocks to n characters. Each entry prints its card first. A card ends with its trust labels: the provenance and the evidence state. A card can end with | body omitted. Run bundle with its ID to get the body. A requested block always prints, also when it is longer. Then the over budget line gives the size. The truncated line names the entries that have no card. It is not in this limit. The default is 8000."],
|
|
57
57
|
["--depth <n>", "Follow links n steps. The default is 1."],
|
|
58
58
|
["--all", "Show all copies of an ID."],
|
|
59
59
|
["--block", "Print only the requirement block, as it sits in the spec file. Each ID must be a requirement."],
|
|
@@ -90,6 +90,11 @@ export const TABLE = [
|
|
|
90
90
|
flags: [["--change <name>", "Show only this change."], ["--write", "Also write docs/metrics/<date>.md."]],
|
|
91
91
|
exit: PLAIN, example: "specctx stats --write",
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: "report", group: "Read", args: "", summary: "Print a bug report from the run journal in .ctx/local. Review it, then file it with `gh issue create`. Write nothing.",
|
|
95
|
+
flags: [],
|
|
96
|
+
exit: [OK, ["2", "A usage error. The command has no JSON output."], INTERNAL], example: "specctx report > report.md",
|
|
97
|
+
},
|
|
93
98
|
{
|
|
94
99
|
name: "lint tells", group: "Read", args: "<file...>", summary: "Find the AI writing tells in markdown files. Read stdin when no file is given.",
|
|
95
100
|
flags: LINT_FLAGS, exit: LINT_EXIT, example: "specctx lint tells --baseline 0 README.md",
|
|
@@ -101,8 +106,8 @@ export const TABLE = [
|
|
|
101
106
|
// Write
|
|
102
107
|
{ name: "build", group: "Write", args: "", summary: "Write the index to .ctx/index.json.", flags: [], exit: PLAIN, example: "specctx build" },
|
|
103
108
|
{
|
|
104
|
-
name: "id new", group: "Write", args: "<kind> <code>", summary: "Print the next free ID. The kind is REQ, SCN, ADR, or DEC.",
|
|
105
|
-
flags: [["--for <REQ-ID>", "Give the requirement of a new scenario."], ["--count <n>", "Print n IDs in sequence."]],
|
|
109
|
+
name: "id new", group: "Write", args: "<kind> <code>", summary: "Print the next free ID. The kind is REQ, SCN, ADR, or DEC. A second call before `build` prints the same ID.",
|
|
110
|
+
flags: [["--for <REQ-ID>", "Give the requirement of a new scenario."], ["--count <n>", "Print n IDs in sequence. Use it to get several IDs before `build`."]],
|
|
106
111
|
exit: [OK, ["2", "A usage error or an unknown code."], INTERNAL], example: "specctx id new REQ IDX",
|
|
107
112
|
},
|
|
108
113
|
{
|
|
@@ -206,7 +211,9 @@ export const HELP = overview();
|
|
|
206
211
|
/** REQ-CLI-006 the page of one command: Usage, Flags, Exit codes, Example. */
|
|
207
212
|
export function commandPage(c) {
|
|
208
213
|
const lint = c.name.startsWith("lint ");
|
|
209
|
-
|
|
214
|
+
// REQ-JRN-008 the report page lists no --json, because report has no JSON output
|
|
215
|
+
const global = c.name === "report" ? GLOBAL.filter(([f]) => f !== "--json") : GLOBAL;
|
|
216
|
+
const flags = lint ? c.flags : [...c.flags, ...global];
|
|
210
217
|
const out = [
|
|
211
218
|
`specctx ${c.name}: ${c.summary}`,
|
|
212
219
|
"",
|
package/dist/cli/workflow.js
CHANGED
|
@@ -171,10 +171,13 @@ export async function runWorkflow(c) {
|
|
|
171
171
|
// REQ-LOOP-002 the fail file is the retry record. At the cap, one question stops the loop (024/D4, 024/D5).
|
|
172
172
|
const { writeFail, openQuestionFor, capQuestion, blockedLine } = await import("../loop/fails.js");
|
|
173
173
|
const { addQuestion } = await import("../handoff/questions.js");
|
|
174
|
-
|
|
174
|
+
// REQ-TASK-008 the fail file and the cap question hold no machine path. The printed tail stays as it is (056/D5).
|
|
175
|
+
const { redactPaths } = await import("../util/redact.js");
|
|
176
|
+
const kept = redactPaths(outTail, c.root);
|
|
177
|
+
const fail = writeFail(c.root, t.id, { command: t.verify.command, exit: r.timedOut ? null : r.exit }, kept);
|
|
175
178
|
let question = openQuestionFor(c.root, t.id)?.n ?? null;
|
|
176
179
|
if (question === null && fail.file.attempt >= c.cfg.loop.max_attempts) {
|
|
177
|
-
question = addQuestion(c.root, t.id, capQuestion(t.id, fail.file.attempt,
|
|
180
|
+
question = addQuestion(c.root, t.id, capQuestion(t.id, fail.file.attempt, kept), "cap").n;
|
|
178
181
|
}
|
|
179
182
|
if (c.json)
|
|
180
183
|
print(c, { attempt: fail.file.attempt, command: t.verify.command, error: msg, exit: r.exit, fail: fail.rel, question, tail: outTail, task: t.id });
|
|
@@ -196,8 +199,10 @@ export async function runWorkflow(c) {
|
|
|
196
199
|
// the Verify command can change tagged files, so the digests come from the tree after the run
|
|
197
200
|
const { loadIndex } = await import("../index/build.js");
|
|
198
201
|
const after = loadIndex(c.root, c.cfg).data.entries;
|
|
202
|
+
// REQ-TASK-008 the stderr tail holds no machine path. verify.command stays as the task line has it (056/D5, 056/D6).
|
|
203
|
+
const { redactPaths } = await import("../util/redact.js");
|
|
199
204
|
const rel = writeEvidence(c.root, t, after, {
|
|
200
|
-
command: t.verify.command, exit: 0, stdout_sha256: sha256(r.stdout), stderr_tail: tail(r.stderr.toString("utf8")),
|
|
205
|
+
command: t.verify.command, exit: 0, stdout_sha256: sha256(r.stdout), stderr_tail: redactPaths(tail(r.stderr.toString("utf8")), c.root),
|
|
201
206
|
}, "command", undefined, t.verify.cwd);
|
|
202
207
|
setBox(c.root, t, true);
|
|
203
208
|
clearCurrent(c.root, t.id);
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// REQ-CLI-001 bin entry. tests call main() from src/main.ts in process.
|
|
3
|
+
// REQ-JRN-001 this entry writes the run journal after main() returns. main() only fills the sink (057/D1).
|
|
3
4
|
import { main } from "./main.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
4
6
|
process.stdout.on("error", (e) => {
|
|
5
7
|
if (e.code === "EPIPE")
|
|
6
8
|
process.exit(0);
|
|
7
9
|
});
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const argv = process.argv.slice(2);
|
|
11
|
+
const sink = { signals: new Set() };
|
|
12
|
+
const t0 = performance.now();
|
|
13
|
+
let code;
|
|
14
|
+
let thrown;
|
|
15
|
+
let failed = false;
|
|
16
|
+
try {
|
|
17
|
+
code = await main(argv, {
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
out: (s) => process.stdout.write(s),
|
|
20
|
+
err: (s) => process.stderr.write(s),
|
|
21
|
+
tty: process.stdout.isTTY === true,
|
|
22
|
+
journal: sink,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
failed = true;
|
|
27
|
+
thrown = e;
|
|
28
|
+
code = 3;
|
|
29
|
+
try {
|
|
30
|
+
const { errInfo } = await import("./journal/note.js");
|
|
31
|
+
sink.err = errInfo(e);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// no journal detail
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
// REQ-JRN-001 a usage error before the root check keeps no root. Only the "?" command with no --root looks for one (057/D2).
|
|
39
|
+
if (sink.root === undefined && sink.cmd === "?" && !argv.some((a) => a === "--root" || a.startsWith("--root="))) {
|
|
40
|
+
const { findRoot } = await import("./cli/root.js");
|
|
41
|
+
sink.root = findRoot(process.cwd());
|
|
42
|
+
}
|
|
43
|
+
if (sink.root !== undefined && process.env.SPECCTX_JOURNAL !== "0") {
|
|
44
|
+
const { appendRun } = await import("./journal/journal.js");
|
|
45
|
+
appendRun(sink, { exit: code, ms: performance.now() - t0, version: VERSION });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// the journal never changes the exit code or the output
|
|
50
|
+
}
|
|
51
|
+
if (failed)
|
|
52
|
+
throw thrown;
|
|
14
53
|
process.exitCode = code;
|