@descent-vtt/spec-harness 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +35 -0
- package/README.md +4 -1
- package/dist/branch.js +2 -0
- package/dist/branch.js.map +1 -1
- package/dist/briefs.js +2 -0
- package/dist/briefs.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/git.d.ts +8 -6
- package/dist/git.js +11 -6
- package/dist/git.js.map +1 -1
- package/dist/reader.js +4 -0
- package/dist/reader.js.map +1 -1
- package/dist/sandbox.d.ts +20 -1
- package/dist/sandbox.js +74 -24
- package/dist/sandbox.js.map +1 -1
- package/package.json +3 -3
- package/skills/draft-brief/SKILL.md +7 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Rounds of agent work under a brief: context, a guard on every write, escalations a person signs, an audit at the end. Needs Claude Code 2.1.139 or later, and @descent-vtt/spec-harness and @descent-vtt/spec-brief 0.2.0 or later installed in the project.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DescentVTT",
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,41 @@ only fixes, reports less or documents, so `^0.7.0` takes only releases that
|
|
|
9
9
|
cannot turn a run red. The family's policy is
|
|
10
10
|
[spec-core's ADR-0009](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md).
|
|
11
11
|
|
|
12
|
+
## 0.10.1
|
|
13
|
+
|
|
14
|
+
On Windows a probe lost its verdict when its worktree could not be deleted,
|
|
15
|
+
and Ctrl+Break left its commands running; and the README's probe example
|
|
16
|
+
started its tool through `npx`.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- The README's probe example ran its test through `npx` by the runner's name
|
|
21
|
+
alone, and the `draft-brief` skill said nothing of how a probe starts a
|
|
22
|
+
tool. A probe runs in a fresh worktree with nothing installed until its
|
|
23
|
+
`setup` installs it and no terminal, where `npx <tool>` fetches the
|
|
24
|
+
registry's package of that name and runs it, unasked: the example is now
|
|
25
|
+
`run: npm test -- tests/probes/rotate.test.ts`, and the skill tells an
|
|
26
|
+
agent to write a probe so
|
|
27
|
+
([ADR-0007](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0007-probes-declare-their-failure.md)).
|
|
28
|
+
Upgrading: in a brief of your own, have a probe's `run` and `setup` start
|
|
29
|
+
a tool through one of the project's scripts, or through
|
|
30
|
+
`npx --no-install <tool>`. `probe` itself runs a line as it is written.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- `probe` keeps its verdict when a worktree's directory cannot be deleted,
|
|
35
|
+
as on Windows while something a probe's command started is still running
|
|
36
|
+
in it. It ended in a Node stack trace with exit 1 before, the whole
|
|
37
|
+
checkout left in the temporary directory and git's record of the worktree
|
|
38
|
+
left in the repository. Git now forgets the worktree first, `probe` prints
|
|
39
|
+
and exits by its verdict, and it names the directory it left on the
|
|
40
|
+
standard error as it exits
|
|
41
|
+
([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
|
|
42
|
+
- Ctrl+Break on Windows, which GitHub's runner also sends a step it
|
|
43
|
+
cancels, stops a `probe`'s commands and removes its worktree, as Ctrl+C
|
|
44
|
+
does, and exits 149. It ended the harness at once before, with the
|
|
45
|
+
worktree in place and the commands running on.
|
|
46
|
+
|
|
12
47
|
## 0.10.0
|
|
13
48
|
|
|
14
49
|
The skills and the README gave `npx` the tools' bare command names, which on
|
package/README.md
CHANGED
|
@@ -263,7 +263,7 @@ A defect is measured before it is filed. The brief carries its probe:
|
|
|
263
263
|
```probe
|
|
264
264
|
id: old-token-still-accepted
|
|
265
265
|
setup: npm ci
|
|
266
|
-
run:
|
|
266
|
+
run: npm test -- tests/probes/rotate.test.ts
|
|
267
267
|
signature: expected 401, got 200
|
|
268
268
|
```
|
|
269
269
|
|
|
@@ -274,8 +274,10 @@ signature: expected 401, got 200
|
|
|
274
274
|
|
|
275
275
|
- `probe --at base` runs it in a temporary worktree at the base commit: every run must fail **for the declared reason** - the `signature` in the output, or a JUnit `test` failing - or the verdict is `vacuous` (no defect), `flaky` or `invalid`.
|
|
276
276
|
- `probe --at head` must be green: `fixed`.
|
|
277
|
+
- That worktree is a fresh checkout, so a probe's commands can rely on the files the commit tracks, the probe's own files and what its `setup` installed, and on nothing else: there is no `node_modules` until `setup` puts one there. Start a tool through one of the project's scripts, as `npm test --` does above, or through `npx --no-install <tool>`. Given a tool's name alone, `npx` fetches the registry's package of that name wherever `setup` did not install the tool, and runs it, unasked: the command has no terminal to be asked on ([Names](#names)).
|
|
277
278
|
- The evidence table it prints names the hash of the probe it measured with ([ADR-0007](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0007-probes-declare-their-failure.md)).
|
|
278
279
|
- Interrupted, it stops every command it started, with everything those started, before it removes the worktree ([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
|
|
280
|
+
- A worktree's directory that cannot be deleted - on Windows, one that something a command started is still running in - costs the verdict nothing: git forgets the worktree, `probe` prints and exits as it would have, and it names the directory it left on the standard error as it exits.
|
|
279
281
|
|
|
280
282
|
### `premises`
|
|
281
283
|
|
|
@@ -425,6 +427,7 @@ A person who runs the tools by hand runs `npx --no-install @descent-vtt/spec-har
|
|
|
425
427
|
| `130` | A `probe` interrupted by SIGINT (Ctrl+C) while it runs. |
|
|
426
428
|
| `143` | A `probe` interrupted by SIGTERM. |
|
|
427
429
|
| `129` | A `probe` interrupted by SIGHUP. |
|
|
430
|
+
| `149` | A `probe` interrupted by SIGBREAK, which is Ctrl+Break on Windows. |
|
|
428
431
|
|
|
429
432
|
An interrupted `probe` exits as a shell reports a process the signal ended.
|
|
430
433
|
|
package/dist/branch.js
CHANGED
package/dist/branch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../src/branch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which brief a branch is working on, read from its name.\n *\n * A round runs on a branch, and naming the branch after the brief is the one\n * convention every repository that runs agents on briefs already has. Reading\n * it from there keeps the harness stateless: no file says which brief is\n * active, so no file can say it wrongly (ADR-0004).\n *\n * A template is a branch name with `{id}` where the id goes and `*` for any\n * run of characters other than `/`: `brief/{id}`, `*\\/brief-{id}`. The id is\n * the longest run of letters and digits at that point, and must be followed\n * by the end of the name or a separator - so `brief/012-rotate-tokens` names\n * 012, and `brief/012x` names 012x, never 012.\n */\n\nconst SEPARATORS = new Set(['-', '_', '/', '.']);\n\nfunction isIdChar(ch: string): boolean {\n return /^[A-Za-z0-9]$/.test(ch);\n}\n\n/**\n * The offsets in `name` at which a template prefix can end, by dynamic\n * programming over prefix and name, so a template of many stars costs their\n * product and never an exponent.\n */\nfunction prefixEnds(prefix: string, name: string): number[] {\n // reach[j]: the prefix read so far can end at name offset j.\n let reach: boolean[] = new Array<boolean>(name.length + 1).fill(false);\n reach[0] = true;\n for (const token of prefix) {\n const next = new Array<boolean>(name.length + 1).fill(false);\n if (token === '*') {\n let open = false;\n for (let j = 0; j <= name.length; j += 1) {\n if (reach[j]) open = true;\n if (open) next[j] = true;\n if (name.charAt(j) === '/') open = false;\n }\n } else {\n for (let j = 0; j < name.length; j += 1) if (reach[j] && name.charAt(j) === token) next[j + 1] = true;\n }\n reach = next;\n }\n const ends: number[] = [];\n reach.forEach((ok, j) => {\n if (ok) ends.push(j);\n });\n return ends;\n}\n\n/** Why a template cannot be used, or `null`. */\nexport function templateError(template: string): string | null {\n const count = template.split('{id}').length - 1;\n if (count !== 1) return `\"${template}\" must contain {id} exactly once`;\n if (!template.endsWith('{id}')) return `\"${template}\" must end with {id}; whatever follows the id is the branch's own`;\n return null;\n}\n\n/** The brief id a branch name carries under one template, or `null`. */\nexport function idFromBranch(template: string, name: string): string | null {\n if (templateError(template) !== null) return null;\n const prefix = template.slice(0, -'{id}'.length);\n for (const start of prefixEnds(prefix, name)) {\n let end = start;\n while (end < name.length && isIdChar(name.charAt(end))) end += 1;\n if (end === start) continue;\n if (end < name.length && !SEPARATORS.has(name.charAt(end))) continue;\n return name.slice(start, end);\n }\n return null;\n}\n\n/**\n * The branch a CI run is building, as the forge names it, for a checkout on\n * no branch: CI checks out a commit, and without the branch a round's own\n * run would name no brief. In order: a GitHub pull request's head branch; the\n * branch a GitHub push built; a GitLab merge request's source branch; the\n * branch a GitLab pipeline built. Each is set only where it names a branch -\n * GitHub's `GITHUB_REF_NAME` names a tag too, so only when `GITHUB_REF_TYPE`\n * says `branch` - and an empty one is not set. `null` outside CI.\n */\nexport function ciBranch(env: Readonly<Record<string, string | undefined>>): { readonly branch: string; readonly source: string } | null {\n const named = (name: string): { branch: string; source: string } | null => {\n const value = env[name]?.trim() ?? '';\n return value === '' ? null : { branch: value, source: name };\n };\n return (\n named('GITHUB_HEAD_REF') ??\n (env['GITHUB_REF_TYPE'] === 'branch' ? named('GITHUB_REF_NAME') : null) ??\n named('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') ??\n named('CI_COMMIT_BRANCH')\n );\n}\n\n/** The first template that yields an id, in order; `null` for a head on no branch, which names no brief. */\nexport function briefIdFromBranch(templates: readonly string[], name: string | null): string | null {\n if (name === null) return null;\n for (const template of templates) {\n const id = idFromBranch(template, name);\n if (id !== null) return id;\n }\n return null;\n}\n\n/**\n * Whether two ids name the same brief: equal as written, or equal as numbers\n * when both are all digits, since a branch says `brief/12` of a brief whose\n * file says `012`.\n */\nexport function sameId(a: string, b: string): boolean {\n if (a === b) return true;\n return /^\\d+$/.test(a) && /^\\d+$/.test(b) && BigInt(a) === BigInt(b);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../src/branch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which brief a branch is working on, read from its name.\n *\n * A round runs on a branch, and naming the branch after the brief is the one\n * convention every repository that runs agents on briefs already has. Reading\n * it from there keeps the harness stateless: no file says which brief is\n * active, so no file can say it wrongly (ADR-0004).\n *\n * A template is a branch name with `{id}` where the id goes and `*` for any\n * run of characters other than `/`: `brief/{id}`, `*\\/brief-{id}`. The id is\n * the longest run of letters and digits at that point, and must be followed\n * by the end of the name or a separator - so `brief/012-rotate-tokens` names\n * 012, and `brief/012x` names 012x, never 012.\n */\n\nconst SEPARATORS = new Set(['-', '_', '/', '.']);\n\nfunction isIdChar(ch: string): boolean {\n // Asked of one character at a time, so the mutants that drop either anchor\n // are equivalent.\n return /^[A-Za-z0-9]$/.test(ch);\n}\n\n/**\n * The offsets in `name` at which a template prefix can end, by dynamic\n * programming over prefix and name, so a template of many stars costs their\n * product and never an exponent.\n */\nfunction prefixEnds(prefix: string, name: string): number[] {\n // reach[j]: the prefix read so far can end at name offset j.\n let reach: boolean[] = new Array<boolean>(name.length + 1).fill(false);\n reach[0] = true;\n for (const token of prefix) {\n const next = new Array<boolean>(name.length + 1).fill(false);\n if (token === '*') {\n let open = false;\n for (let j = 0; j <= name.length; j += 1) {\n if (reach[j]) open = true;\n if (open) next[j] = true;\n if (name.charAt(j) === '/') open = false;\n }\n } else {\n for (let j = 0; j < name.length; j += 1) if (reach[j] && name.charAt(j) === token) next[j + 1] = true;\n }\n reach = next;\n }\n const ends: number[] = [];\n reach.forEach((ok, j) => {\n if (ok) ends.push(j);\n });\n return ends;\n}\n\n/** Why a template cannot be used, or `null`. */\nexport function templateError(template: string): string | null {\n const count = template.split('{id}').length - 1;\n if (count !== 1) return `\"${template}\" must contain {id} exactly once`;\n if (!template.endsWith('{id}')) return `\"${template}\" must end with {id}; whatever follows the id is the branch's own`;\n return null;\n}\n\n/** The brief id a branch name carries under one template, or `null`. */\nexport function idFromBranch(template: string, name: string): string | null {\n if (templateError(template) !== null) return null;\n const prefix = template.slice(0, -'{id}'.length);\n for (const start of prefixEnds(prefix, name)) {\n let end = start;\n while (end < name.length && isIdChar(name.charAt(end))) end += 1;\n if (end === start) continue;\n if (end < name.length && !SEPARATORS.has(name.charAt(end))) continue;\n return name.slice(start, end);\n }\n return null;\n}\n\n/**\n * The branch a CI run is building, as the forge names it, for a checkout on\n * no branch: CI checks out a commit, and without the branch a round's own\n * run would name no brief. In order: a GitHub pull request's head branch; the\n * branch a GitHub push built; a GitLab merge request's source branch; the\n * branch a GitLab pipeline built. Each is set only where it names a branch -\n * GitHub's `GITHUB_REF_NAME` names a tag too, so only when `GITHUB_REF_TYPE`\n * says `branch` - and an empty one is not set. `null` outside CI.\n */\nexport function ciBranch(env: Readonly<Record<string, string | undefined>>): { readonly branch: string; readonly source: string } | null {\n const named = (name: string): { branch: string; source: string } | null => {\n const value = env[name]?.trim() ?? '';\n return value === '' ? null : { branch: value, source: name };\n };\n return (\n named('GITHUB_HEAD_REF') ??\n (env['GITHUB_REF_TYPE'] === 'branch' ? named('GITHUB_REF_NAME') : null) ??\n named('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') ??\n named('CI_COMMIT_BRANCH')\n );\n}\n\n/** The first template that yields an id, in order; `null` for a head on no branch, which names no brief. */\nexport function briefIdFromBranch(templates: readonly string[], name: string | null): string | null {\n if (name === null) return null;\n for (const template of templates) {\n const id = idFromBranch(template, name);\n if (id !== null) return id;\n }\n return null;\n}\n\n/**\n * Whether two ids name the same brief: equal as written, or equal as numbers\n * when both are all digits, since a branch says `brief/12` of a brief whose\n * file says `012`.\n */\nexport function sameId(a: string, b: string): boolean {\n if (a === b) return true;\n return /^\\d+$/.test(a) && /^\\d+$/.test(b) && BigInt(a) === BigInt(b);\n}\n"]}
|
package/dist/briefs.js
CHANGED
|
@@ -42,6 +42,8 @@ export function parseBriefList(document) {
|
|
|
42
42
|
throw new SiblingOutputError('spec-brief did not print a list document');
|
|
43
43
|
}
|
|
44
44
|
const version = document['schemaVersion'];
|
|
45
|
+
// The list holds numbers alone, so `includes` refuses whatever is not one:
|
|
46
|
+
// the `typeof` is the compiler's, and the mutant without it is equivalent.
|
|
45
47
|
if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {
|
|
46
48
|
throw new SiblingOutputError(`spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`);
|
|
47
49
|
}
|
package/dist/briefs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"briefs.js","sourceRoot":"","sources":["../src/briefs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The briefs, as spec-brief reports them, and which one a round is working on.\n *\n * spec-brief owns the brief format and its configuration (its ADR-0003), so\n * the harness reads briefs the way spec-brief does by asking it:\n * `spec-brief list --archived --format json`. This module reads that\n * document and nothing else; a shape it does not recognise is an error, never\n * a best-effort reading of half the fields (ADR-0002).\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\n/**\n * The spec-brief JSON document versions this harness reads. Version 1 is the\n * one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the\n * harness never reads, so the `list` and `archive` documents are the same.\n */\nexport const SUPPORTED_SCHEMA_VERSIONS: readonly number[] = [1, 2, 3];\n\nexport class SiblingOutputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingOutputError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction strings(value: unknown, where: string): string[] {\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {\n throw new SiblingOutputError(`${where} is not a list of strings`);\n }\n return value;\n}\n\nfunction nullableString(value: unknown, where: string): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') throw new SiblingOutputError(`${where} is not a string`);\n return value;\n}\n\n/** Reads the document `spec-brief list --format json` prints. */\nexport function parseBriefList(document: unknown): BriefRow[] {\n if (!isObject(document) || document['tool'] !== 'spec-brief' || document['command'] !== 'list') {\n throw new SiblingOutputError('spec-brief did not print a list document');\n }\n const version = document['schemaVersion'];\n if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {\n throw new SiblingOutputError(\n `spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`,\n );\n }\n const briefs = document['briefs'];\n if (!Array.isArray(briefs)) throw new SiblingOutputError('spec-brief list has no \"briefs\"');\n return briefs.map((row, index) => {\n const where = `briefs[${index}]`;\n if (!isObject(row)) throw new SiblingOutputError(`${where} is not an object`);\n const id = row['id'];\n const file = row['file'];\n if (typeof id !== 'string' || typeof file !== 'string') throw new SiblingOutputError(`${where} has no id or file`);\n const phase = row['phase'];\n if (phase !== 'live' && phase !== 'archived') throw new SiblingOutputError(`${where}.phase is \"${String(phase)}\"`);\n const tasks = row['tasks'];\n const wave = row['wave'];\n return {\n id,\n file,\n title: nullableString(row['title'], `${where}.title`),\n phase,\n status: nullableString(row['status'], `${where}.status`),\n type: nullableString(row['type'], `${where}.type`),\n wave: typeof wave === 'number' ? wave : null,\n dependsOn: strings(row['dependsOn'] ?? [], `${where}.dependsOn`),\n affectedFiles: strings(row['affectedFiles'] ?? [], `${where}.affectedFiles`),\n protectedFiles: strings(row['protectedFiles'] ?? [], `${where}.protectedFiles`),\n tasks: {\n total: isObject(tasks) && typeof tasks['total'] === 'number' ? tasks['total'] : 0,\n checked: isObject(tasks) && typeof tasks['checked'] === 'number' ? tasks['checked'] : 0,\n },\n ready: row['ready'] === true,\n waitingOn: strings(row['waitingOn'] ?? [], `${where}.waitingOn`),\n };\n });\n}\n\n/** How the active brief was named, for messages. */\nexport type ActiveSource = 'flag' | 'environment' | 'branch';\n\nexport type ActiveBrief =\n | { readonly kind: 'found'; readonly brief: BriefRow; readonly source: ActiveSource }\n | { readonly kind: 'none'; readonly reason: string }\n | { readonly kind: 'unknown'; readonly id: string; readonly source: ActiveSource }\n | { readonly kind: 'archived'; readonly brief: BriefRow; readonly source: ActiveSource };\n\n/**\n * The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by\n * the branch - never guessed from which brief happens to be the only active\n * one (ADR-0004).\n */\nexport function findActive(\n briefs: readonly BriefRow[],\n named: { readonly flag?: string | undefined; readonly environment?: string | undefined; readonly branch?: string | null | undefined },\n): ActiveBrief {\n const pick = (id: string, source: ActiveSource): ActiveBrief => {\n const matches = briefs.filter((brief) => sameId(brief.id, id));\n const live = matches.find((brief) => brief.phase === 'live');\n if (live !== undefined) return { kind: 'found', brief: live, source };\n const archived = matches[0];\n if (archived !== undefined) return { kind: 'archived', brief: archived, source };\n return { kind: 'unknown', id, source };\n };\n if (named.flag !== undefined && named.flag.trim() !== '') return pick(named.flag.trim(), 'flag');\n if (named.environment !== undefined && named.environment.trim() !== '') return pick(named.environment.trim(), 'environment');\n if (named.branch !== undefined && named.branch !== null) return pick(named.branch, 'branch');\n return {\n kind: 'none',\n reason: 'no brief is named: pass --brief <id>, set SPEC_BRIEF, or work on a branch named like brief/<id>-<topic>',\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"briefs.js","sourceRoot":"","sources":["../src/briefs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAEtE,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAID,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,OAAO,CAAC,KAAc,EAAE,KAAa;IAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC9E,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,2BAA2B,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CAAC,KAAc,EAAE,KAAa;IACnD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,kBAAkB,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,QAAiB;IAC9C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,YAAY,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,MAAM,EAAE,CAAC;QAC/F,MAAM,IAAI,kBAAkB,CAAC,0CAA0C,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IAC1C,2EAA2E;IAC3E,2EAA2E;IAC3E,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,kBAAkB,CAC1B,oCAAoC,MAAM,CAAC,OAAO,CAAC,wBAAwB,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClH,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,kBAAkB,CAAC,iCAAiC,CAAC,CAAC;IAC5F,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,mBAAmB,CAAC,CAAC;QAC9E,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACrB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,oBAAoB,CAAC,CAAC;QACnH,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,UAAU;YAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,cAAc,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnH,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,OAAO;YACL,EAAE;YACF,IAAI;YACJ,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,QAAQ,CAAC;YACrD,KAAK;YACL,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,SAAS,CAAC;YACxD,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;YAClD,IAAI,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;YAC5C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,YAAY,CAAC;YAChE,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,gBAAgB,CAAC;YAC5E,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,iBAAiB,CAAC;YAC/E,KAAK,EAAE;gBACL,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjF,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;aACxF;YACD,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI;YAC5B,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,YAAY,CAAC;SACjE,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,MAA2B,EAC3B,KAAqI;IAErI,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,MAAoB,EAAe,EAAE;QAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;QAC7D,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACjF,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC,CAAC;IACF,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IACjG,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,CAAC,CAAC;IAC7H,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC7F,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,yGAAyG;KAClH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The briefs, as spec-brief reports them, and which one a round is working on.\n *\n * spec-brief owns the brief format and its configuration (its ADR-0003), so\n * the harness reads briefs the way spec-brief does by asking it:\n * `spec-brief list --archived --format json`. This module reads that\n * document and nothing else; a shape it does not recognise is an error, never\n * a best-effort reading of half the fields (ADR-0002).\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\n/**\n * The spec-brief JSON document versions this harness reads. Version 1 is the\n * one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the\n * harness never reads, so the `list` and `archive` documents are the same.\n */\nexport const SUPPORTED_SCHEMA_VERSIONS: readonly number[] = [1, 2, 3];\n\nexport class SiblingOutputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingOutputError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction strings(value: unknown, where: string): string[] {\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {\n throw new SiblingOutputError(`${where} is not a list of strings`);\n }\n return value;\n}\n\nfunction nullableString(value: unknown, where: string): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') throw new SiblingOutputError(`${where} is not a string`);\n return value;\n}\n\n/** Reads the document `spec-brief list --format json` prints. */\nexport function parseBriefList(document: unknown): BriefRow[] {\n if (!isObject(document) || document['tool'] !== 'spec-brief' || document['command'] !== 'list') {\n throw new SiblingOutputError('spec-brief did not print a list document');\n }\n const version = document['schemaVersion'];\n // The list holds numbers alone, so `includes` refuses whatever is not one:\n // the `typeof` is the compiler's, and the mutant without it is equivalent.\n if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {\n throw new SiblingOutputError(\n `spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`,\n );\n }\n const briefs = document['briefs'];\n if (!Array.isArray(briefs)) throw new SiblingOutputError('spec-brief list has no \"briefs\"');\n return briefs.map((row, index) => {\n const where = `briefs[${index}]`;\n if (!isObject(row)) throw new SiblingOutputError(`${where} is not an object`);\n const id = row['id'];\n const file = row['file'];\n if (typeof id !== 'string' || typeof file !== 'string') throw new SiblingOutputError(`${where} has no id or file`);\n const phase = row['phase'];\n if (phase !== 'live' && phase !== 'archived') throw new SiblingOutputError(`${where}.phase is \"${String(phase)}\"`);\n const tasks = row['tasks'];\n const wave = row['wave'];\n return {\n id,\n file,\n title: nullableString(row['title'], `${where}.title`),\n phase,\n status: nullableString(row['status'], `${where}.status`),\n type: nullableString(row['type'], `${where}.type`),\n wave: typeof wave === 'number' ? wave : null,\n dependsOn: strings(row['dependsOn'] ?? [], `${where}.dependsOn`),\n affectedFiles: strings(row['affectedFiles'] ?? [], `${where}.affectedFiles`),\n protectedFiles: strings(row['protectedFiles'] ?? [], `${where}.protectedFiles`),\n tasks: {\n total: isObject(tasks) && typeof tasks['total'] === 'number' ? tasks['total'] : 0,\n checked: isObject(tasks) && typeof tasks['checked'] === 'number' ? tasks['checked'] : 0,\n },\n ready: row['ready'] === true,\n waitingOn: strings(row['waitingOn'] ?? [], `${where}.waitingOn`),\n };\n });\n}\n\n/** How the active brief was named, for messages. */\nexport type ActiveSource = 'flag' | 'environment' | 'branch';\n\nexport type ActiveBrief =\n | { readonly kind: 'found'; readonly brief: BriefRow; readonly source: ActiveSource }\n | { readonly kind: 'none'; readonly reason: string }\n | { readonly kind: 'unknown'; readonly id: string; readonly source: ActiveSource }\n | { readonly kind: 'archived'; readonly brief: BriefRow; readonly source: ActiveSource };\n\n/**\n * The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by\n * the branch - never guessed from which brief happens to be the only active\n * one (ADR-0004).\n */\nexport function findActive(\n briefs: readonly BriefRow[],\n named: { readonly flag?: string | undefined; readonly environment?: string | undefined; readonly branch?: string | null | undefined },\n): ActiveBrief {\n const pick = (id: string, source: ActiveSource): ActiveBrief => {\n const matches = briefs.filter((brief) => sameId(brief.id, id));\n const live = matches.find((brief) => brief.phase === 'live');\n if (live !== undefined) return { kind: 'found', brief: live, source };\n const archived = matches[0];\n if (archived !== undefined) return { kind: 'archived', brief: archived, source };\n return { kind: 'unknown', id, source };\n };\n if (named.flag !== undefined && named.flag.trim() !== '') return pick(named.flag.trim(), 'flag');\n if (named.environment !== undefined && named.environment.trim() !== '') return pick(named.environment.trim(), 'environment');\n if (named.branch !== undefined && named.branch !== null) return pick(named.branch, 'branch');\n return {\n kind: 'none',\n reason: 'no brief is named: pass --brief <id>, set SPEC_BRIEF, or work on a branch named like brief/<id>-<topic>',\n };\n}\n"]}
|
package/dist/config.js
CHANGED
|
@@ -156,6 +156,8 @@ export function parseConfig(raw, file = CONFIG_FILE) {
|
|
|
156
156
|
const command = value[name];
|
|
157
157
|
if (command === undefined || command === null)
|
|
158
158
|
continue;
|
|
159
|
+
// A list that is not empty has a first entry: the `?.` is the
|
|
160
|
+
// compiler's, and the mutant without it is equivalent.
|
|
159
161
|
if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {
|
|
160
162
|
// A sibling runs without a shell, so the example is node and the
|
|
161
163
|
// script: npx is a shim Windows cannot start without one.
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `.spec-harness.json` may say, the defaults, and validation.\n *\n * Every key has a default, so a repository without the file works; a file\n * that says something this version does not understand stops the run with\n * exit 2, because a guard that silently ignored a misspelt key would be\n * guarding something other than what its owner wrote.\n */\n\nimport { templateError } from './branch.js';\n\nexport type OutOfScope = 'warn' | 'ask' | 'deny';\n\nexport interface HarnessConfig {\n /** Branch-name templates that carry the active brief's id (ADR-0004). */\n readonly branches: readonly string[];\n /** What a write outside the brief's `affectedFiles` gets: a warning, a question to the person, or a refusal. */\n readonly outOfScope: OutOfScope;\n /** The branch rounds merge into, which audits and rulings are measured against. `null`: the remote's default branch. */\n readonly base: string | null;\n readonly rulings: {\n /** The brief section whose table holds the rulings. */\n readonly section: string;\n /** The allowed-signers file, read from the base branch, never from the round's own tree. */\n readonly allowedSigners: string;\n };\n readonly dependencies: {\n /** Manifest file names whose added dependencies an audit reports. */\n readonly manifests: readonly string[];\n };\n readonly context: {\n /** The most a context packet may hold, in characters. Cited documents fill what the rest leaves; those that do not fit are named by path. */\n readonly budget: number;\n };\n readonly assertions: {\n /**\n * Brief sections whose assertions state what was true before the round -\n * the defect, measured - and are expected to stop holding when it ends.\n * Every other assertion in a brief is a goal.\n */\n readonly premises: readonly string[];\n };\n readonly probes: {\n /** How many times a probe runs; every run must agree. */\n readonly runs: number;\n /** Seconds one run may take. */\n readonly timeout: number;\n };\n /** The command for each sibling tool, or `null` to find it (ADR-0002). */\n readonly tools: Readonly<Record<SiblingName, readonly string[] | null>>;\n}\n\nexport type SiblingName = 'spec-brief' | 'spec-graph' | 'spec-guard';\nexport const SIBLINGS: readonly SiblingName[] = ['spec-brief', 'spec-graph', 'spec-guard'];\n\nexport const CONFIG_FILE = '.spec-harness.json';\n\nexport const DEFAULT_MANIFESTS: readonly string[] = [\n 'package.json',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'requirements*.txt',\n 'Directory.Packages.props',\n '*.csproj',\n '*.fsproj',\n '*.vbproj',\n 'Gemfile',\n];\n\nexport const DEFAULT_CONFIG: HarnessConfig = {\n branches: ['brief/{id}', 'brief-{id}', '*/brief/{id}', '*/brief-{id}'],\n outOfScope: 'warn',\n base: null,\n rulings: { section: 'Rulings', allowedSigners: '.github/allowed_signers' },\n dependencies: { manifests: DEFAULT_MANIFESTS },\n context: { budget: 60_000 },\n assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },\n probes: { runs: 2, timeout: 600 },\n tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },\n};\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStringList(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}\n\nfunction checkKeys(value: Json, allowed: readonly string[], where: string): void {\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) {\n throw new ConfigError(`${where}: unknown key \"${key}\"; the keys are ${allowed.join(', ')}`);\n }\n }\n}\n\nfunction positive(value: unknown, where: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new ConfigError(`${where} must be a whole number of at least 1`);\n }\n return value;\n}\n\nfunction nonEmpty(value: unknown, where: string): string {\n if (typeof value !== 'string' || value.trim() === '') throw new ConfigError(`${where} must be a non-empty string`);\n return value;\n}\n\n/** Validates a parsed configuration and fills in every default. */\nexport function parseConfig(raw: unknown, file = CONFIG_FILE): HarnessConfig {\n if (!isObject(raw)) throw new ConfigError(`${file} must hold a JSON object`);\n checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools'], file);\n\n let branches = DEFAULT_CONFIG.branches;\n if (raw['branches'] !== undefined) {\n if (!isStringList(raw['branches']) || raw['branches'].length === 0) {\n throw new ConfigError(`${file}: \"branches\" must be a non-empty list of templates`);\n }\n for (const template of raw['branches']) {\n const error = templateError(template);\n if (error !== null) throw new ConfigError(`${file}: \"branches\": ${error}`);\n }\n branches = raw['branches'];\n }\n\n let outOfScope = DEFAULT_CONFIG.outOfScope;\n if (raw['outOfScope'] !== undefined) {\n if (raw['outOfScope'] !== 'warn' && raw['outOfScope'] !== 'ask' && raw['outOfScope'] !== 'deny') {\n throw new ConfigError(`${file}: \"outOfScope\" must be \"warn\", \"ask\" or \"deny\"`);\n }\n outOfScope = raw['outOfScope'];\n }\n\n let base = DEFAULT_CONFIG.base;\n if (raw['base'] !== undefined && raw['base'] !== null) base = nonEmpty(raw['base'], `${file}: \"base\"`);\n\n let rulings = DEFAULT_CONFIG.rulings;\n if (raw['rulings'] !== undefined) {\n const value = raw['rulings'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"rulings\" must be an object`);\n checkKeys(value, ['section', 'allowedSigners'], `${file}: \"rulings\"`);\n rulings = {\n section: value['section'] === undefined ? rulings.section : nonEmpty(value['section'], `${file}: \"rulings.section\"`),\n allowedSigners:\n value['allowedSigners'] === undefined\n ? rulings.allowedSigners\n : nonEmpty(value['allowedSigners'], `${file}: \"rulings.allowedSigners\"`),\n };\n }\n\n let dependencies = DEFAULT_CONFIG.dependencies;\n if (raw['dependencies'] !== undefined) {\n const value = raw['dependencies'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"dependencies\" must be an object`);\n checkKeys(value, ['manifests'], `${file}: \"dependencies\"`);\n if (value['manifests'] !== undefined) {\n if (!isStringList(value['manifests'])) throw new ConfigError(`${file}: \"dependencies.manifests\" must be a list of file names`);\n dependencies = { manifests: value['manifests'] };\n }\n }\n\n let context = DEFAULT_CONFIG.context;\n if (raw['context'] !== undefined) {\n const value = raw['context'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"context\" must be an object`);\n checkKeys(value, ['budget'], `${file}: \"context\"`);\n if (value['budget'] !== undefined) context = { budget: positive(value['budget'], `${file}: \"context.budget\"`) };\n }\n\n let assertions = DEFAULT_CONFIG.assertions;\n if (raw['assertions'] !== undefined) {\n const value = raw['assertions'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"assertions\" must be an object`);\n checkKeys(value, ['premises'], `${file}: \"assertions\"`);\n if (value['premises'] !== undefined) {\n if (!isStringList(value['premises'])) throw new ConfigError(`${file}: \"assertions.premises\" must be a list of section names`);\n assertions = { premises: value['premises'] };\n }\n }\n\n let probes = DEFAULT_CONFIG.probes;\n if (raw['probes'] !== undefined) {\n const value = raw['probes'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"probes\" must be an object`);\n checkKeys(value, ['runs', 'timeout'], `${file}: \"probes\"`);\n probes = {\n runs: value['runs'] === undefined ? probes.runs : positive(value['runs'], `${file}: \"probes.runs\"`),\n timeout: value['timeout'] === undefined ? probes.timeout : positive(value['timeout'], `${file}: \"probes.timeout\"`),\n };\n }\n\n const tools: Record<SiblingName, readonly string[] | null> = { ...DEFAULT_CONFIG.tools };\n if (raw['tools'] !== undefined) {\n const value = raw['tools'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"tools\" must be an object`);\n checkKeys(value, SIBLINGS, `${file}: \"tools\"`);\n for (const name of SIBLINGS) {\n const command = value[name];\n if (command === undefined || command === null) continue;\n if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {\n // A sibling runs without a shell, so the example is node and the\n // script: npx is a shim Windows cannot start without one.\n throw new ConfigError(`${file}: \"tools.${name}\" must be a command as a list, such as [\"node\", \"node_modules/@descent-vtt/${name}/bin/${name}.js\"], or null`);\n }\n tools[name] = command;\n }\n }\n\n return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA4C5C,MAAM,CAAC,MAAM,QAAQ,GAA2B,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC;AAE3F,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAEhD,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,cAAc;IACd,YAAY;IACZ,QAAQ;IACR,gBAAgB;IAChB,mBAAmB;IACnB,0BAA0B;IAC1B,UAAU;IACV,UAAU;IACV,UAAU;IACV,SAAS;CACV,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAkB;IAC3C,QAAQ,EAAE,CAAC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,CAAC;IACtE,UAAU,EAAE,MAAM;IAClB,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,yBAAyB,EAAE;IAC1E,YAAY,EAAE,EAAE,SAAS,EAAE,iBAAiB,EAAE;IAC9C,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;IAC3B,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,sBAAsB,EAAE,UAAU,EAAE,eAAe,CAAC,EAAE;IAC/E,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE;IACjC,KAAK,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE;CACtE,CAAC;AAEF,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAID,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;AACjF,CAAC;AAED,SAAS,SAAS,CAAC,KAAW,EAAE,OAA0B,EAAE,KAAa;IACvE,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,kBAAkB,GAAG,mBAAmB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc,EAAE,KAAa;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,uCAAuC,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc,EAAE,KAAa;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,6BAA6B,CAAC,CAAC;IACnH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAC,GAAY,EAAE,IAAI,GAAG,WAAW;IAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,0BAA0B,CAAC,CAAC;IAC7E,SAAS,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;IAE3I,IAAI,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC;IACvC,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,oDAAoD,CAAC,CAAC;QACrF,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,iBAAiB,KAAK,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC;IAC3C,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,SAAS,EAAE,CAAC;QACpC,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,MAAM,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,KAAK,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,MAAM,EAAE,CAAC;YAChG,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,gDAAgD,CAAC,CAAC;QACjF,CAAC;QACD,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC;IAC/B,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI;QAAE,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,UAAU,CAAC,CAAC;IAEvG,IAAI,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC;IACrC,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,+BAA+B,CAAC,CAAC;QACpF,SAAS,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,GAAG,IAAI,aAAa,CAAC,CAAC;QACtE,OAAO,GAAG;YACR,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,qBAAqB,CAAC;YACpH,cAAc,EACZ,KAAK,CAAC,gBAAgB,CAAC,KAAK,SAAS;gBACnC,CAAC,CAAC,OAAO,CAAC,cAAc;gBACxB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,GAAG,IAAI,4BAA4B,CAAC;SAC7E,CAAC;IACJ,CAAC;IAED,IAAI,YAAY,GAAG,cAAc,CAAC,YAAY,CAAC;IAC/C,IAAI,GAAG,CAAC,cAAc,CAAC,KAAK,SAAS,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,oCAAoC,CAAC,CAAC;QACzF,SAAS,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,EAAE,GAAG,IAAI,kBAAkB,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,yDAAyD,CAAC,CAAC;YAC/H,YAAY,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;QACnD,CAAC;IACH,CAAC;IAED,IAAI,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC;IACrC,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,+BAA+B,CAAC,CAAC;QACpF,SAAS,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,aAAa,CAAC,CAAC;QACnD,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,SAAS;YAAE,OAAO,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,oBAAoB,CAAC,EAAE,CAAC;IAClH,CAAC;IAED,IAAI,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC;IAC3C,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,kCAAkC,CAAC,CAAC;QACvF,SAAS,CAAC,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,gBAAgB,CAAC,CAAC;QACxD,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,SAAS,EAAE,CAAC;YACpC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,yDAAyD,CAAC,CAAC;YAC9H,UAAU,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,IAAI,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC;IACnC,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,8BAA8B,CAAC,CAAC;QACnF,SAAS,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,IAAI,YAAY,CAAC,CAAC;QAC3D,MAAM,GAAG;YACP,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,iBAAiB,CAAC;YACnG,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,oBAAoB,CAAC;SACnH,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAkD,EAAE,GAAG,cAAc,CAAC,KAAK,EAAE,CAAC;IACzF,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;QAClF,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,WAAW,CAAC,CAAC;QAC/C,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5B,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI;gBAAE,SAAS;YACxD,8DAA8D;YAC9D,uDAAuD;YACvD,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBAChF,iEAAiE;gBACjE,0DAA0D;gBAC1D,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,YAAY,IAAI,8EAA8E,IAAI,QAAQ,IAAI,gBAAgB,CAAC,CAAC;YAC/J,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;QACxB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACnG,CAAC","sourcesContent":["/**\n * What `.spec-harness.json` may say, the defaults, and validation.\n *\n * Every key has a default, so a repository without the file works; a file\n * that says something this version does not understand stops the run with\n * exit 2, because a guard that silently ignored a misspelt key would be\n * guarding something other than what its owner wrote.\n */\n\nimport { templateError } from './branch.js';\n\nexport type OutOfScope = 'warn' | 'ask' | 'deny';\n\nexport interface HarnessConfig {\n /** Branch-name templates that carry the active brief's id (ADR-0004). */\n readonly branches: readonly string[];\n /** What a write outside the brief's `affectedFiles` gets: a warning, a question to the person, or a refusal. */\n readonly outOfScope: OutOfScope;\n /** The branch rounds merge into, which audits and rulings are measured against. `null`: the remote's default branch. */\n readonly base: string | null;\n readonly rulings: {\n /** The brief section whose table holds the rulings. */\n readonly section: string;\n /** The allowed-signers file, read from the base branch, never from the round's own tree. */\n readonly allowedSigners: string;\n };\n readonly dependencies: {\n /** Manifest file names whose added dependencies an audit reports. */\n readonly manifests: readonly string[];\n };\n readonly context: {\n /** The most a context packet may hold, in characters. Cited documents fill what the rest leaves; those that do not fit are named by path. */\n readonly budget: number;\n };\n readonly assertions: {\n /**\n * Brief sections whose assertions state what was true before the round -\n * the defect, measured - and are expected to stop holding when it ends.\n * Every other assertion in a brief is a goal.\n */\n readonly premises: readonly string[];\n };\n readonly probes: {\n /** How many times a probe runs; every run must agree. */\n readonly runs: number;\n /** Seconds one run may take. */\n readonly timeout: number;\n };\n /** The command for each sibling tool, or `null` to find it (ADR-0002). */\n readonly tools: Readonly<Record<SiblingName, readonly string[] | null>>;\n}\n\nexport type SiblingName = 'spec-brief' | 'spec-graph' | 'spec-guard';\nexport const SIBLINGS: readonly SiblingName[] = ['spec-brief', 'spec-graph', 'spec-guard'];\n\nexport const CONFIG_FILE = '.spec-harness.json';\n\nexport const DEFAULT_MANIFESTS: readonly string[] = [\n 'package.json',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'requirements*.txt',\n 'Directory.Packages.props',\n '*.csproj',\n '*.fsproj',\n '*.vbproj',\n 'Gemfile',\n];\n\nexport const DEFAULT_CONFIG: HarnessConfig = {\n branches: ['brief/{id}', 'brief-{id}', '*/brief/{id}', '*/brief-{id}'],\n outOfScope: 'warn',\n base: null,\n rulings: { section: 'Rulings', allowedSigners: '.github/allowed_signers' },\n dependencies: { manifests: DEFAULT_MANIFESTS },\n context: { budget: 60_000 },\n assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },\n probes: { runs: 2, timeout: 600 },\n tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },\n};\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStringList(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}\n\nfunction checkKeys(value: Json, allowed: readonly string[], where: string): void {\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) {\n throw new ConfigError(`${where}: unknown key \"${key}\"; the keys are ${allowed.join(', ')}`);\n }\n }\n}\n\nfunction positive(value: unknown, where: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new ConfigError(`${where} must be a whole number of at least 1`);\n }\n return value;\n}\n\nfunction nonEmpty(value: unknown, where: string): string {\n if (typeof value !== 'string' || value.trim() === '') throw new ConfigError(`${where} must be a non-empty string`);\n return value;\n}\n\n/** Validates a parsed configuration and fills in every default. */\nexport function parseConfig(raw: unknown, file = CONFIG_FILE): HarnessConfig {\n if (!isObject(raw)) throw new ConfigError(`${file} must hold a JSON object`);\n checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools'], file);\n\n let branches = DEFAULT_CONFIG.branches;\n if (raw['branches'] !== undefined) {\n if (!isStringList(raw['branches']) || raw['branches'].length === 0) {\n throw new ConfigError(`${file}: \"branches\" must be a non-empty list of templates`);\n }\n for (const template of raw['branches']) {\n const error = templateError(template);\n if (error !== null) throw new ConfigError(`${file}: \"branches\": ${error}`);\n }\n branches = raw['branches'];\n }\n\n let outOfScope = DEFAULT_CONFIG.outOfScope;\n if (raw['outOfScope'] !== undefined) {\n if (raw['outOfScope'] !== 'warn' && raw['outOfScope'] !== 'ask' && raw['outOfScope'] !== 'deny') {\n throw new ConfigError(`${file}: \"outOfScope\" must be \"warn\", \"ask\" or \"deny\"`);\n }\n outOfScope = raw['outOfScope'];\n }\n\n let base = DEFAULT_CONFIG.base;\n if (raw['base'] !== undefined && raw['base'] !== null) base = nonEmpty(raw['base'], `${file}: \"base\"`);\n\n let rulings = DEFAULT_CONFIG.rulings;\n if (raw['rulings'] !== undefined) {\n const value = raw['rulings'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"rulings\" must be an object`);\n checkKeys(value, ['section', 'allowedSigners'], `${file}: \"rulings\"`);\n rulings = {\n section: value['section'] === undefined ? rulings.section : nonEmpty(value['section'], `${file}: \"rulings.section\"`),\n allowedSigners:\n value['allowedSigners'] === undefined\n ? rulings.allowedSigners\n : nonEmpty(value['allowedSigners'], `${file}: \"rulings.allowedSigners\"`),\n };\n }\n\n let dependencies = DEFAULT_CONFIG.dependencies;\n if (raw['dependencies'] !== undefined) {\n const value = raw['dependencies'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"dependencies\" must be an object`);\n checkKeys(value, ['manifests'], `${file}: \"dependencies\"`);\n if (value['manifests'] !== undefined) {\n if (!isStringList(value['manifests'])) throw new ConfigError(`${file}: \"dependencies.manifests\" must be a list of file names`);\n dependencies = { manifests: value['manifests'] };\n }\n }\n\n let context = DEFAULT_CONFIG.context;\n if (raw['context'] !== undefined) {\n const value = raw['context'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"context\" must be an object`);\n checkKeys(value, ['budget'], `${file}: \"context\"`);\n if (value['budget'] !== undefined) context = { budget: positive(value['budget'], `${file}: \"context.budget\"`) };\n }\n\n let assertions = DEFAULT_CONFIG.assertions;\n if (raw['assertions'] !== undefined) {\n const value = raw['assertions'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"assertions\" must be an object`);\n checkKeys(value, ['premises'], `${file}: \"assertions\"`);\n if (value['premises'] !== undefined) {\n if (!isStringList(value['premises'])) throw new ConfigError(`${file}: \"assertions.premises\" must be a list of section names`);\n assertions = { premises: value['premises'] };\n }\n }\n\n let probes = DEFAULT_CONFIG.probes;\n if (raw['probes'] !== undefined) {\n const value = raw['probes'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"probes\" must be an object`);\n checkKeys(value, ['runs', 'timeout'], `${file}: \"probes\"`);\n probes = {\n runs: value['runs'] === undefined ? probes.runs : positive(value['runs'], `${file}: \"probes.runs\"`),\n timeout: value['timeout'] === undefined ? probes.timeout : positive(value['timeout'], `${file}: \"probes.timeout\"`),\n };\n }\n\n const tools: Record<SiblingName, readonly string[] | null> = { ...DEFAULT_CONFIG.tools };\n if (raw['tools'] !== undefined) {\n const value = raw['tools'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"tools\" must be an object`);\n checkKeys(value, SIBLINGS, `${file}: \"tools\"`);\n for (const name of SIBLINGS) {\n const command = value[name];\n if (command === undefined || command === null) continue;\n // A list that is not empty has a first entry: the `?.` is the\n // compiler's, and the mutant without it is equivalent.\n if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {\n // A sibling runs without a shell, so the example is node and the\n // script: npx is a shim Windows cannot start without one.\n throw new ConfigError(`${file}: \"tools.${name}\" must be a command as a list, such as [\"node\", \"node_modules/@descent-vtt/${name}/bin/${name}.js\"], or null`);\n }\n tools[name] = command;\n }\n }\n\n return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools };\n}\n"]}
|
package/dist/git.d.ts
CHANGED
|
@@ -69,10 +69,12 @@ export type Signature = {
|
|
|
69
69
|
export declare function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature>;
|
|
70
70
|
export declare function addWorktree(path: string, rev: string, cwd: string): Promise<void>;
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* Has git remove the worktree at `path`, and no other, with its record of it,
|
|
73
|
+
* and answers whether git did. Git deletes what it can of the worktree's
|
|
74
|
+
* directory and forgets the worktree even when it cannot delete it all. It
|
|
75
|
+
* refuses a worktree whose directory has lost its `.git` file, and removes
|
|
76
|
+
* its record of one whose directory is gone, found by the path it was added
|
|
77
|
+
* at: `git worktree prune` would also forget every other worktree git has
|
|
78
|
+
* lost track of, the person's among them.
|
|
77
79
|
*/
|
|
78
|
-
export declare function removeWorktree(path: string, cwd: string): Promise<
|
|
80
|
+
export declare function removeWorktree(path: string, cwd: string): Promise<boolean>;
|
package/dist/git.js
CHANGED
|
@@ -147,13 +147,18 @@ export async function addWorktree(path, rev, cwd) {
|
|
|
147
147
|
throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
150
|
+
* Has git remove the worktree at `path`, and no other, with its record of it,
|
|
151
|
+
* and answers whether git did. Git deletes what it can of the worktree's
|
|
152
|
+
* directory and forgets the worktree even when it cannot delete it all. It
|
|
153
|
+
* refuses a worktree whose directory has lost its `.git` file, and removes
|
|
154
|
+
* its record of one whose directory is gone, found by the path it was added
|
|
155
|
+
* at: `git worktree prune` would also forget every other worktree git has
|
|
156
|
+
* lost track of, the person's among them.
|
|
155
157
|
*/
|
|
156
158
|
export async function removeWorktree(path, cwd) {
|
|
157
|
-
|
|
159
|
+
// The sandbox asks again when this answers false, and git, asked again
|
|
160
|
+
// about a worktree it has forgotten, changes nothing: the mutant that
|
|
161
|
+
// always answers false is equivalent.
|
|
162
|
+
return (await git(['worktree', 'remove', '--force', path], cwd)).code === 0;
|
|
158
163
|
}
|
|
159
164
|
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string): Promise<Run> {\n return new Promise((resolve) => {\n // windowsHide only keeps a console window from flashing up on Windows,\n // and the sweep runs on Linux, where its mutant is equivalent.\n execFile('git', [...args], { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true }, (error, stdout, stderr) => {\n // Every caller tells 0 from any other code, never one failure from\n // another, so which code a failure gets is equivalent to its mutants.\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? (error as { code: number }).code : -1;\n resolve({ code, stdout, stderr });\n });\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/**\n * Where git runs the hook `name` from, absolute: `core.hooksPath` read as git\n * reads it - relative to the work tree, absolute, or under `~` - and a linked\n * worktree's shared hooks. `null` when git cannot say, before 2.31.\n */\nexport async function hookPath(name: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-path', `hooks/${name}`], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\n/** Every local branch, by its short name. */\nexport async function localBranches(cwd: string): Promise<string[]> {\n const listed = await value(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd);\n // for-each-ref fails only where git itself does, outside a repository,\n // where init never runs: the null check is there for the type.\n return listed === null || listed === '' ? [] : listed.split('\\n');\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] as string;\n if (code === '') {\n i += 1;\n continue;\n }\n const status = code.charAt(0) as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/** Whether a commit id from {@link blameLine} is the all-zero one of a line nobody committed. */\nexport function isUncommitted(sha: string): boolean {\n return /^0+$/.test(sha);\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n // A blame that fails prints nothing on stdout, which names no commit. Its\n // output opens with the commit, so the anchor is equivalent to its mutant.\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\n/** A good signature names its allowed signer's principal; any other names none. */\nexport type Signature =\n | { readonly good: true; readonly principal: string; readonly detail: string }\n | { readonly good: false; readonly principal: null; readonly detail: string };\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1];\n // git writes the check to stderr, so the trim changes the first line only\n // for output on stdout alone, which verify-commit does not print; and split\n // always answers a first part.\n const detail = text.trim().split('\\n')[0] as string;\n // git exits 0 only for a signature by a listed principal, which it names:\n // either condition alone is equivalent to both, and both trust neither.\n return run.code === 0 && principal !== undefined ? { good: true, principal, detail } : { good: false, principal: null, detail };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\n/**\n * Forgets the worktree at `path`, and no other, once its directory is\n * deleted. Git refuses to remove a worktree whose directory has lost its\n * `.git` file, and removes its record of one whose directory is gone, found by\n * the path it was added at: `git worktree prune` would also forget every other\n * worktree git has lost track of, the person's among them.\n */\nexport async function removeWorktree(path: string, cwd: string): Promise<void> {\n await git(['worktree', 'remove', '--force', path], cwd);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string): Promise<Run> {\n return new Promise((resolve) => {\n // windowsHide only keeps a console window from flashing up on Windows,\n // and the sweep runs on Linux, where its mutant is equivalent.\n execFile('git', [...args], { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true }, (error, stdout, stderr) => {\n // Every caller tells 0 from any other code, never one failure from\n // another, so which code a failure gets is equivalent to its mutants.\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? (error as { code: number }).code : -1;\n resolve({ code, stdout, stderr });\n });\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/**\n * Where git runs the hook `name` from, absolute: `core.hooksPath` read as git\n * reads it - relative to the work tree, absolute, or under `~` - and a linked\n * worktree's shared hooks. `null` when git cannot say, before 2.31.\n */\nexport async function hookPath(name: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-path', `hooks/${name}`], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\n/** Every local branch, by its short name. */\nexport async function localBranches(cwd: string): Promise<string[]> {\n const listed = await value(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd);\n // for-each-ref fails only where git itself does, outside a repository,\n // where init never runs: the null check is there for the type.\n return listed === null || listed === '' ? [] : listed.split('\\n');\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] as string;\n if (code === '') {\n i += 1;\n continue;\n }\n const status = code.charAt(0) as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/** Whether a commit id from {@link blameLine} is the all-zero one of a line nobody committed. */\nexport function isUncommitted(sha: string): boolean {\n return /^0+$/.test(sha);\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n // A blame that fails prints nothing on stdout, which names no commit. Its\n // output opens with the commit, so the anchor is equivalent to its mutant.\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\n/** A good signature names its allowed signer's principal; any other names none. */\nexport type Signature =\n | { readonly good: true; readonly principal: string; readonly detail: string }\n | { readonly good: false; readonly principal: null; readonly detail: string };\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1];\n // git writes the check to stderr, so the trim changes the first line only\n // for output on stdout alone, which verify-commit does not print; and split\n // always answers a first part.\n const detail = text.trim().split('\\n')[0] as string;\n // git exits 0 only for a signature by a listed principal, which it names:\n // either condition alone is equivalent to both, and both trust neither.\n return run.code === 0 && principal !== undefined ? { good: true, principal, detail } : { good: false, principal: null, detail };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\n/**\n * Has git remove the worktree at `path`, and no other, with its record of it,\n * and answers whether git did. Git deletes what it can of the worktree's\n * directory and forgets the worktree even when it cannot delete it all. It\n * refuses a worktree whose directory has lost its `.git` file, and removes\n * its record of one whose directory is gone, found by the path it was added\n * at: `git worktree prune` would also forget every other worktree git has\n * lost track of, the person's among them.\n */\nexport async function removeWorktree(path: string, cwd: string): Promise<boolean> {\n // The sandbox asks again when this answers false, and git, asked again\n // about a worktree it has forgotten, changes nothing: the mutant that\n // always answers false is equivalent.\n return (await git(['worktree', 'remove', '--force', path], cwd)).code === 0;\n}\n"]}
|
package/dist/reader.js
CHANGED
|
@@ -6,6 +6,8 @@ import { findEntry, readFrontMatter, scanMarkdown } from './vendor/spec-core/mar
|
|
|
6
6
|
import { splitLines } from './vendor/spec-core/text/index.js';
|
|
7
7
|
/** Section names compare without case, emphasis, a leading number or a trailing colon. */
|
|
8
8
|
export function sameSection(heading, name) {
|
|
9
|
+
// Both names are folded the same way and then compared, so the mutant that
|
|
10
|
+
// folds them to upper case is equivalent.
|
|
9
11
|
const normal = (text) => text
|
|
10
12
|
.replace(/[*_`]/g, '')
|
|
11
13
|
.replace(/^\s*\d+[.)]?\s+/, '')
|
|
@@ -36,6 +38,8 @@ function scanOf(text, cache) {
|
|
|
36
38
|
}
|
|
37
39
|
export function createReader() {
|
|
38
40
|
// Commands read the same brief several times in a row; one scan serves them.
|
|
41
|
+
// It spares a scan and changes no answer, so the mutant that keeps nothing
|
|
42
|
+
// is equivalent.
|
|
39
43
|
let last = null;
|
|
40
44
|
const scan = (text) => {
|
|
41
45
|
const result = scanOf(text, last);
|
package/dist/reader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAuC,MAAM,sCAAsC,CAAC;AACrI,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAE9D,0FAA0F;AAC1F,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,IAAY;IACvD,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE,CACtC,IAAI;SACD,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;SAC9B,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;SACtB,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;IACnB,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,IAAY,EAAE,KAAyB;IAC1D,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,QAAQ;SAClB,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;SAChE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAW,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;AAClH,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,KAAkD;IAC9E,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,6EAA6E;IAC7E,IAAI,IAAI,GAAgD,IAAI,CAAC;IAC7D,MAAM,IAAI,GAAG,CAAC,IAAY,EAAgB,EAAE;QAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;IAEF,OAAO;QACL,UAAU,CAAC,IAAY;YACrB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3B,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE,CAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAqB,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACzF,OAAO,OAAO,CAAC,MAAM;iBAClB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;iBAC1C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;gBACb,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;gBAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;gBAC9D,mEAAmE;gBACnE,mEAAmE;gBACnE,mEAAmE;gBACnE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;gBACpE,MAAM,KAAK,GAAa,EAAE,CAAC;gBAC3B,KAAK,IAAI,IAAI,GAAG,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;oBAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC;oBACvD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/D,CAAC;gBACD,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3E,CAAC,CAAC,CAAC;QACP,CAAC;QAED,SAAS,CAAC,IAAY,EAAE,IAAY;YAClC,IAAI,KAAK,GAAkB,IAAI,CAAC;YAChC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAC1C,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI;oBAAE,MAAM;gBAC/B,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC;oBAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;YAC/C,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,UAAU,CAAC,IAAY,EAAE,IAAY;YACnC,MAAM,IAAI,GAAsC,EAAE,CAAC;YACnD,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAC1C,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI;oBAAE,MAAM;gBAC/B,IAAI,OAAO,CAAC,KAAK,GAAG,CAAC;oBAAE,SAAS;gBAChC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAK,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAuB,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK;oBAAE,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC1G,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1D,CAAC;YACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;QAED,aAAa,CAAC,IAAY,EAAE,OAAe;YACzC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;YAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;YACxG,IAAI,KAAK,GAAG,CAAC;gBAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;YAC5E,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAA8B,CAAC;YAC7D,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;YAC7F,MAAM,OAAO,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAC7E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;YACtG,MAAM,KAAK,GAAgB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAChD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;gBAC/C,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;aAC/F,CAAC,CAAC,CAAC;YACJ,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC5C,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QAC3H,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,OAAO,IAAI,CAAC,IAAI,CAAC;iBACd,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,CAAC;iBAC7F,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC/D,CAAC;QAED,cAAc,CAAC,IAAY;YAMzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC;YACpF,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,MAAM,GAAkB,IAAI,CAAC;YACjC,sEAAsE;YACtE,2CAA2C;YAC3C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC;YACnG,wEAAwE;YACxE,wEAAwE;YACxE,OAAO;gBACL,KAAK;gBACL,MAAM;gBACN,mBAAmB,EAAE,OAAO,CAAC,mBAAmB,EAAE,IAAI,KAAK,MAAM;gBACjE,qBAAqB,EAAE,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC;aAChD,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n // Both names are folded the same way and then compared, so the mutant that\n // folds them to upper case is equivalent.\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n // It spares a scan and changes no answer, so the mutant that keeps nothing\n // is equivalent.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -8,8 +8,27 @@
|
|
|
8
8
|
* succeeded, threw, or the process was interrupted (ADR-0003). The person's
|
|
9
9
|
* branch, index, stash and working files are never touched; a failed round is
|
|
10
10
|
* discarded by discarding its worktree, never by resetting theirs.
|
|
11
|
+
*
|
|
12
|
+
* Git forgets a worktree whatever becomes of its directory. A directory that
|
|
13
|
+
* cannot be deleted, as on Windows while something a command started still
|
|
14
|
+
* runs in it, costs the job nothing: its answer stands, the directory is
|
|
15
|
+
* tried again as the process exits, and one still there then is named on the
|
|
16
|
+
* standard error.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Runs `work` in a detached worktree of `repository` at `revision`, and
|
|
20
|
+
* removes the worktree after.
|
|
21
|
+
*
|
|
22
|
+
* Git removes it first, while it is as the job left it: git then forgets the
|
|
23
|
+
* worktree whether or not all of its directory can be deleted. What git left
|
|
24
|
+
* of the directory is deleted next, which is all of it when the job broke the
|
|
25
|
+
* worktree, since git refuses one whose `.git` file is gone. Only then is
|
|
26
|
+
* git asked again, once the directory is gone, when it forgets what it finds
|
|
27
|
+
* missing (`removeWorktree`).
|
|
28
|
+
*
|
|
29
|
+
* A directory that cannot be deleted stays on the list for the exit to try
|
|
30
|
+
* again, and does not take the job's answer with it.
|
|
11
31
|
*/
|
|
12
|
-
/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */
|
|
13
32
|
export declare function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T>;
|
|
14
33
|
export interface CommandRun {
|
|
15
34
|
/** `null` when the command was stopped at its timeout. */
|
package/dist/sandbox.js
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* succeeded, threw, or the process was interrupted (ADR-0003). The person's
|
|
9
9
|
* branch, index, stash and working files are never touched; a failed round is
|
|
10
10
|
* discarded by discarding its worktree, never by resetting theirs.
|
|
11
|
+
*
|
|
12
|
+
* Git forgets a worktree whatever becomes of its directory. A directory that
|
|
13
|
+
* cannot be deleted, as on Windows while something a command started still
|
|
14
|
+
* runs in it, costs the job nothing: its answer stands, the directory is
|
|
15
|
+
* tried again as the process exits, and one still there then is named on the
|
|
16
|
+
* standard error.
|
|
11
17
|
*/
|
|
12
18
|
import { execFileSync, spawn } from 'node:child_process';
|
|
13
19
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
@@ -61,23 +67,23 @@ function stop(child) {
|
|
|
61
67
|
}
|
|
62
68
|
/**
|
|
63
69
|
* Deletes a worktree's directory, trying again for up to `SETTLE_MS` while
|
|
64
|
-
* something holds it
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* deleted all the same.
|
|
70
|
+
* something holds it, and answers `null` once it is gone, or else why it is
|
|
71
|
+
* not. On Windows a command just stopped lets go of its directory a moment
|
|
72
|
+
* after taskkill returns, and an exit handler, which runs synchronously, has
|
|
73
|
+
* no other way to wait; elsewhere a directory in use is deleted all the same.
|
|
68
74
|
*/
|
|
69
75
|
function discard(directory) {
|
|
70
76
|
const deadline = Date.now() + SETTLE_MS;
|
|
71
77
|
for (;;) {
|
|
72
78
|
try {
|
|
73
79
|
rmSync(directory, { recursive: true, force: true });
|
|
74
|
-
return;
|
|
80
|
+
return null;
|
|
75
81
|
}
|
|
76
82
|
catch (error) {
|
|
77
83
|
// `>=` would differ only in the millisecond the deadline falls on, so
|
|
78
84
|
// that mutant is equivalent.
|
|
79
85
|
if (Date.now() > deadline)
|
|
80
|
-
|
|
86
|
+
return error;
|
|
81
87
|
// The pause only spares the processor between attempts: without it
|
|
82
88
|
// the directory is deleted, or given up on, at the same moment, so
|
|
83
89
|
// that mutant is equivalent.
|
|
@@ -85,28 +91,48 @@ function discard(directory) {
|
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Has git remove the worktree at `directory` with its record of it, as
|
|
96
|
+
* `removeWorktree` does and synchronously, and answers whether git did.
|
|
97
|
+
*/
|
|
98
|
+
function forget(directory, repository) {
|
|
99
|
+
try {
|
|
100
|
+
execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });
|
|
101
|
+
// Asked again about a worktree it has forgotten, git changes nothing, so
|
|
102
|
+
// the mutant that answers false here is equivalent.
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Git refused a worktree its job broke, could not delete all of the
|
|
107
|
+
// directory, holds nothing of it any more, or cannot be run. The answer
|
|
108
|
+
// is read with `!` and nowhere else, so the mutant that answers nothing
|
|
109
|
+
// here is equivalent.
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
88
113
|
/**
|
|
89
114
|
* Stops every command still running, then removes every live worktree:
|
|
90
115
|
* synchronously, the only kind of work an exit handler may do, so at exit
|
|
91
|
-
* nothing waits for the commands to end but `discard`'s retries.
|
|
116
|
+
* nothing waits for the commands to end but `discard`'s retries. A directory
|
|
117
|
+
* that outlasts those is named on the standard error and left: nothing
|
|
118
|
+
* thrown here could be caught, and the next worktree is still to remove.
|
|
92
119
|
*/
|
|
93
120
|
function sweep() {
|
|
94
121
|
for (const child of running)
|
|
95
122
|
stop(child);
|
|
96
123
|
for (const [directory, repository] of live) {
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
discard(directory);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// be done from an exit handler.
|
|
106
|
-
}
|
|
124
|
+
// In the order a job's end takes, for the reasons `withWorktree` gives.
|
|
125
|
+
const forgotten = forget(directory, repository);
|
|
126
|
+
const kept = discard(directory);
|
|
127
|
+
if (kept !== null)
|
|
128
|
+
process.stderr.write(`spec-harness: the temporary worktree at ${directory} could not be deleted and is left there: ${kept.message}\n`);
|
|
129
|
+
// The mutant that always asks again is equivalent, as in `forget`.
|
|
130
|
+
else if (!forgotten)
|
|
131
|
+
forget(directory, repository);
|
|
107
132
|
}
|
|
108
|
-
// A signal handler ends in an exit, which sweeps again over worktrees
|
|
109
|
-
// already gone and forgotten,
|
|
133
|
+
// A signal handler ends in an exit, which sweeps again: over worktrees
|
|
134
|
+
// already gone and forgotten, where keeping them would change nothing, and
|
|
135
|
+
// over one that could not be deleted, which would be named a second time.
|
|
110
136
|
live.clear();
|
|
111
137
|
}
|
|
112
138
|
/**
|
|
@@ -137,10 +163,29 @@ function install() {
|
|
|
137
163
|
// meet Node's default, which ends the process before any 'exit' handler
|
|
138
164
|
// runs, with the worktrees still in place. It runs the same steps again,
|
|
139
165
|
// and the first to finish exits.
|
|
140
|
-
|
|
166
|
+
//
|
|
167
|
+
// SIGBREAK is Windows' own: Ctrl+Break, which GitHub's runner sends there,
|
|
168
|
+
// after Ctrl+C, where it sends SIGTERM elsewhere. Unheard, it ends the
|
|
169
|
+
// process before any 'exit' handler runs, and the commands run on.
|
|
170
|
+
// Elsewhere there is no such signal and nothing calls its listener, so
|
|
171
|
+
// its mutants are equivalent there.
|
|
172
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'])
|
|
141
173
|
process.on(signal, interrupted);
|
|
142
174
|
}
|
|
143
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Runs `work` in a detached worktree of `repository` at `revision`, and
|
|
177
|
+
* removes the worktree after.
|
|
178
|
+
*
|
|
179
|
+
* Git removes it first, while it is as the job left it: git then forgets the
|
|
180
|
+
* worktree whether or not all of its directory can be deleted. What git left
|
|
181
|
+
* of the directory is deleted next, which is all of it when the job broke the
|
|
182
|
+
* worktree, since git refuses one whose `.git` file is gone. Only then is
|
|
183
|
+
* git asked again, once the directory is gone, when it forgets what it finds
|
|
184
|
+
* missing (`removeWorktree`).
|
|
185
|
+
*
|
|
186
|
+
* A directory that cannot be deleted stays on the list for the exit to try
|
|
187
|
+
* again, and does not take the job's answer with it.
|
|
188
|
+
*/
|
|
144
189
|
export async function withWorktree(repository, revision, work) {
|
|
145
190
|
install();
|
|
146
191
|
const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));
|
|
@@ -150,9 +195,14 @@ export async function withWorktree(repository, revision, work) {
|
|
|
150
195
|
return await work(directory);
|
|
151
196
|
}
|
|
152
197
|
finally {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
const forgotten = await removeWorktree(directory, repository);
|
|
199
|
+
if (discard(directory) === null) {
|
|
200
|
+
// Asked again about a worktree it has forgotten, git changes nothing,
|
|
201
|
+
// so the mutant that always asks again is equivalent.
|
|
202
|
+
if (!forgotten)
|
|
203
|
+
await removeWorktree(directory, repository);
|
|
204
|
+
live.delete(directory);
|
|
205
|
+
}
|
|
156
206
|
}
|
|
157
207
|
}
|
|
158
208
|
/**
|
package/dist/sandbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Temporary worktrees: somewhere to run a command at a commit without\n * touching the person's own checkout.\n *\n * The family's tools read git and never write it (spec-core ADR-0005). The one\n * exception is this: a detached worktree under the system's temporary\n * directory, added for one job and removed after it, whether the job\n * succeeded, threw, or the process was interrupted (ADR-0003). The person's\n * branch, index, stash and working files are never touched; a failed round is\n * discarded by discarding its worktree, never by resetting theirs.\n */\n\nimport { execFileSync, spawn, type ChildProcess } from 'node:child_process';\nimport { mkdtempSync, rmSync } from 'node:fs';\nimport { constants, tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { addWorktree, removeWorktree } from './git.js';\n\nconst live = new Map<string, string>();\nconst running = new Set<ChildProcess>();\nlet installed = false;\n/** Set once the process is interrupted: from then on a command's end is never answered. */\nlet ending = false;\n\n/**\n * How long the sandbox waits for the commands it stopped to let go of their\n * output and their directory. They were stopped forced, so this is only the\n * time the system takes to end them; whatever still holds either after it has\n * left the tree, and no wait would end it.\n */\nconst SETTLE_MS = 3_000;\n\n/**\n * Stops a command with everything it started, forced: SIGKILL to its process\n * group on Linux and macOS, taskkill over its tree on Windows.\n *\n * `windowsHide` here and below only keeps a console window from opening on\n * Windows, which nothing reads, so its mutants are equivalent.\n */\nfunction stop(child: ChildProcess): void {\n // No pid means the spawn failed and nothing runs. Past the tick it failed\n // in, the kill below throws on the missing id and the fallback finds no\n // process, so the mutant that lets it through is equivalent; within that\n // tick nothing here stops a command.\n if (child.pid === undefined) return;\n try {\n // False already on Linux and macOS, so the mutants that make it false\n // are equivalent there. On Windows the tests fail without taskkill:\n // stopping the shell alone leaves node holding the output. True already\n // on Windows, so the mutant that makes it true is equivalent there;\n // elsewhere there is no taskkill, and the shell alone is stopped.\n if (process.platform === 'win32') execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });\n else process.kill(-child.pid, 'SIGKILL');\n } catch {\n // Reached when the group or the tree is already gone, and the command's\n // shell with it, as when the command left something holding its output;\n // and on Windows when taskkill cannot be run, where stopping the shell is\n // what is left. No test takes that second path.\n child.kill('SIGKILL');\n }\n}\n\n/**\n * Deletes a worktree's directory, trying again for up to `SETTLE_MS` while\n * something holds it. On Windows a command just stopped lets go of its\n * directory a moment after taskkill returns, and an exit handler, which runs\n * synchronously, has no other way to wait; elsewhere a directory in use is\n * deleted all the same.\n */\nfunction discard(directory: string): void {\n const deadline = Date.now() + SETTLE_MS;\n for (;;) {\n try {\n rmSync(directory, { recursive: true, force: true });\n return;\n } catch (error) {\n // `>=` would differ only in the millisecond the deadline falls on, so\n // that mutant is equivalent.\n if (Date.now() > deadline) throw error;\n // The pause only spares the processor between attempts: without it\n // the directory is deleted, or given up on, at the same moment, so\n // that mutant is equivalent.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n }\n}\n\n/**\n * Stops every command still running, then removes every live worktree:\n * synchronously, the only kind of work an exit handler may do, so at exit\n * nothing waits for the commands to end but `discard`'s retries.\n */\nfunction sweep(): void {\n for (const child of running) stop(child);\n for (const [directory, repository] of live) {\n // Deleted first, as at the end of a job, so that git forgets it however\n // the job left it (`removeWorktree` says why).\n discard(directory);\n try {\n execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });\n } catch {\n // Git holds nothing of it any more, or cannot be run; nothing more can\n // be done from an exit handler.\n }\n }\n // A signal handler ends in an exit, which sweeps again over worktrees\n // already gone and forgotten, so a mutant that keeps them changes nothing.\n live.clear();\n}\n\n/**\n * Stops every running command with its tree, waits a bounded time for them to\n * let go of their output, removes the worktrees, and exits as a shell reports\n * a process the signal ended: 128 and the signal's number, the same on every\n * platform Node runs on. The commands lead process groups of their own on\n * Linux and macOS, so the terminal's signal never reached them.\n */\nasync function interrupted(signal: NodeJS.Signals): Promise<void> {\n ending = true;\n const stopping = [...running];\n for (const child of stopping) stop(child);\n await Promise.race([\n Promise.all(stopping.map((child) => new Promise((resolve) => child.once('close', resolve)))),\n new Promise((resolve) => setTimeout(resolve, SETTLE_MS)),\n ]);\n sweep();\n process.exit(128 + constants.signals[signal]);\n}\n\nfunction install(): void {\n if (installed) return;\n installed = true;\n process.once('exit', sweep);\n // `on`, not `once`: a second signal while the first waits would otherwise\n // meet Node's default, which ends the process before any 'exit' handler\n // runs, with the worktrees still in place. It runs the same steps again,\n // and the first to finish exits.\n for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) process.on(signal, interrupted);\n}\n\n/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */\nexport async function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T> {\n install();\n const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));\n live.set(directory, repository);\n try {\n await addWorktree(directory, revision, repository);\n return await work(directory);\n } finally {\n discard(directory);\n await removeWorktree(directory, repository);\n live.delete(directory);\n }\n}\n\nexport interface CommandRun {\n /** `null` when the command was stopped at its timeout. */\n readonly exitCode: number | null;\n readonly output: string;\n}\n\n/**\n * Runs a command line in a directory, output and errors interleaved, stopped\n * with everything it started when it outlives `timeoutSeconds` or the process\n * is interrupted or exits. Once the process is interrupted it never answers:\n * the job waiting on it would otherwise carry on in a worktree being removed.\n *\n * Through the shell, on purpose: a probe's command is a line from the\n * repository's own brief - `npm test -- x` - which the person approved with\n * the brief, exactly as CI runs the repository's own scripts. Nothing from\n * outside the repository reaches it.\n */\nexport function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun> {\n install();\n return new Promise((resolve) => {\n // Elsewhere the command leads a process group of its own, so the timeout\n // can stop the group. Windows has none to signal, and taskkill follows\n // the tree instead; a command detached there loses its output, which the\n // tests catch on Windows. Everywhere else `detached` is true already, so\n // the mutants that make it true are equivalent there, as are\n // `windowsHide`'s.\n const child = spawn(line, { cwd, shell: true, windowsHide: true, detached: process.platform !== 'win32', env: { ...process.env, CI: '1' } });\n running.add(child);\n let output = '';\n const keep = (chunk: Buffer): void => {\n // Enough to find a signature in; a runaway log is not kept whole.\n if (output.length < 4 * 1024 * 1024) output += chunk.toString('utf8');\n };\n // Both streams are piped, so both exist: `?.` only satisfies the type,\n // and its mutants are equivalent.\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n let stopped = false;\n const timer = setTimeout(() => {\n stopped = true;\n stop(child);\n }, timeoutSeconds * 1000);\n child.on('error', (error) => {\n // 'close' follows the error of a spawn that failed, and clears the\n // timer too, so the mutant that drops this call is equivalent.\n clearTimeout(timer);\n resolve({ exitCode: 127, output: `${output}${error.message}\\n` });\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n running.delete(child);\n if (!ending) resolve({ exitCode: stopped ? null : (code ?? 1), output });\n });\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Temporary worktrees: somewhere to run a command at a commit without\n * touching the person's own checkout.\n *\n * The family's tools read git and never write it (spec-core ADR-0005). The one\n * exception is this: a detached worktree under the system's temporary\n * directory, added for one job and removed after it, whether the job\n * succeeded, threw, or the process was interrupted (ADR-0003). The person's\n * branch, index, stash and working files are never touched; a failed round is\n * discarded by discarding its worktree, never by resetting theirs.\n *\n * Git forgets a worktree whatever becomes of its directory. A directory that\n * cannot be deleted, as on Windows while something a command started still\n * runs in it, costs the job nothing: its answer stands, the directory is\n * tried again as the process exits, and one still there then is named on the\n * standard error.\n */\n\nimport { execFileSync, spawn, type ChildProcess } from 'node:child_process';\nimport { mkdtempSync, rmSync } from 'node:fs';\nimport { constants, tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { addWorktree, removeWorktree } from './git.js';\n\nconst live = new Map<string, string>();\nconst running = new Set<ChildProcess>();\nlet installed = false;\n/** Set once the process is interrupted: from then on a command's end is never answered. */\nlet ending = false;\n\n/**\n * How long the sandbox waits for the commands it stopped to let go of their\n * output and their directory. They were stopped forced, so this is only the\n * time the system takes to end them; whatever still holds either after it has\n * left the tree, and no wait would end it.\n */\nconst SETTLE_MS = 3_000;\n\n/**\n * Stops a command with everything it started, forced: SIGKILL to its process\n * group on Linux and macOS, taskkill over its tree on Windows.\n *\n * `windowsHide` here and below only keeps a console window from opening on\n * Windows, which nothing reads, so its mutants are equivalent.\n */\nfunction stop(child: ChildProcess): void {\n // No pid means the spawn failed and nothing runs. Past the tick it failed\n // in, the kill below throws on the missing id and the fallback finds no\n // process, so the mutant that lets it through is equivalent; within that\n // tick nothing here stops a command.\n if (child.pid === undefined) return;\n try {\n // False already on Linux and macOS, so the mutants that make it false\n // are equivalent there. On Windows the tests fail without taskkill:\n // stopping the shell alone leaves node holding the output. True already\n // on Windows, so the mutant that makes it true is equivalent there;\n // elsewhere there is no taskkill, and the shell alone is stopped.\n if (process.platform === 'win32') execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });\n else process.kill(-child.pid, 'SIGKILL');\n } catch {\n // Reached when the group or the tree is already gone, and the command's\n // shell with it, as when the command left something holding its output;\n // and on Windows when taskkill cannot be run, where stopping the shell is\n // what is left. No test takes that second path.\n child.kill('SIGKILL');\n }\n}\n\n/**\n * Deletes a worktree's directory, trying again for up to `SETTLE_MS` while\n * something holds it, and answers `null` once it is gone, or else why it is\n * not. On Windows a command just stopped lets go of its directory a moment\n * after taskkill returns, and an exit handler, which runs synchronously, has\n * no other way to wait; elsewhere a directory in use is deleted all the same.\n */\nfunction discard(directory: string): Error | null {\n const deadline = Date.now() + SETTLE_MS;\n for (;;) {\n try {\n rmSync(directory, { recursive: true, force: true });\n return null;\n } catch (error) {\n // `>=` would differ only in the millisecond the deadline falls on, so\n // that mutant is equivalent.\n if (Date.now() > deadline) return error as Error;\n // The pause only spares the processor between attempts: without it\n // the directory is deleted, or given up on, at the same moment, so\n // that mutant is equivalent.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n }\n}\n\n/**\n * Has git remove the worktree at `directory` with its record of it, as\n * `removeWorktree` does and synchronously, and answers whether git did.\n */\nfunction forget(directory: string, repository: string): boolean {\n try {\n execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });\n // Asked again about a worktree it has forgotten, git changes nothing, so\n // the mutant that answers false here is equivalent.\n return true;\n } catch {\n // Git refused a worktree its job broke, could not delete all of the\n // directory, holds nothing of it any more, or cannot be run. The answer\n // is read with `!` and nowhere else, so the mutant that answers nothing\n // here is equivalent.\n return false;\n }\n}\n\n/**\n * Stops every command still running, then removes every live worktree:\n * synchronously, the only kind of work an exit handler may do, so at exit\n * nothing waits for the commands to end but `discard`'s retries. A directory\n * that outlasts those is named on the standard error and left: nothing\n * thrown here could be caught, and the next worktree is still to remove.\n */\nfunction sweep(): void {\n for (const child of running) stop(child);\n for (const [directory, repository] of live) {\n // In the order a job's end takes, for the reasons `withWorktree` gives.\n const forgotten = forget(directory, repository);\n const kept = discard(directory);\n if (kept !== null) process.stderr.write(`spec-harness: the temporary worktree at ${directory} could not be deleted and is left there: ${kept.message}\\n`);\n // The mutant that always asks again is equivalent, as in `forget`.\n else if (!forgotten) forget(directory, repository);\n }\n // A signal handler ends in an exit, which sweeps again: over worktrees\n // already gone and forgotten, where keeping them would change nothing, and\n // over one that could not be deleted, which would be named a second time.\n live.clear();\n}\n\n/**\n * Stops every running command with its tree, waits a bounded time for them to\n * let go of their output, removes the worktrees, and exits as a shell reports\n * a process the signal ended: 128 and the signal's number, the same on every\n * platform Node runs on. The commands lead process groups of their own on\n * Linux and macOS, so the terminal's signal never reached them.\n */\nasync function interrupted(signal: NodeJS.Signals): Promise<void> {\n ending = true;\n const stopping = [...running];\n for (const child of stopping) stop(child);\n await Promise.race([\n Promise.all(stopping.map((child) => new Promise((resolve) => child.once('close', resolve)))),\n new Promise((resolve) => setTimeout(resolve, SETTLE_MS)),\n ]);\n sweep();\n process.exit(128 + constants.signals[signal]);\n}\n\nfunction install(): void {\n if (installed) return;\n installed = true;\n process.once('exit', sweep);\n // `on`, not `once`: a second signal while the first waits would otherwise\n // meet Node's default, which ends the process before any 'exit' handler\n // runs, with the worktrees still in place. It runs the same steps again,\n // and the first to finish exits.\n //\n // SIGBREAK is Windows' own: Ctrl+Break, which GitHub's runner sends there,\n // after Ctrl+C, where it sends SIGTERM elsewhere. Unheard, it ends the\n // process before any 'exit' handler runs, and the commands run on.\n // Elsewhere there is no such signal and nothing calls its listener, so\n // its mutants are equivalent there.\n for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'] as const) process.on(signal, interrupted);\n}\n\n/**\n * Runs `work` in a detached worktree of `repository` at `revision`, and\n * removes the worktree after.\n *\n * Git removes it first, while it is as the job left it: git then forgets the\n * worktree whether or not all of its directory can be deleted. What git left\n * of the directory is deleted next, which is all of it when the job broke the\n * worktree, since git refuses one whose `.git` file is gone. Only then is\n * git asked again, once the directory is gone, when it forgets what it finds\n * missing (`removeWorktree`).\n *\n * A directory that cannot be deleted stays on the list for the exit to try\n * again, and does not take the job's answer with it.\n */\nexport async function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T> {\n install();\n const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));\n live.set(directory, repository);\n try {\n await addWorktree(directory, revision, repository);\n return await work(directory);\n } finally {\n const forgotten = await removeWorktree(directory, repository);\n if (discard(directory) === null) {\n // Asked again about a worktree it has forgotten, git changes nothing,\n // so the mutant that always asks again is equivalent.\n if (!forgotten) await removeWorktree(directory, repository);\n live.delete(directory);\n }\n }\n}\n\nexport interface CommandRun {\n /** `null` when the command was stopped at its timeout. */\n readonly exitCode: number | null;\n readonly output: string;\n}\n\n/**\n * Runs a command line in a directory, output and errors interleaved, stopped\n * with everything it started when it outlives `timeoutSeconds` or the process\n * is interrupted or exits. Once the process is interrupted it never answers:\n * the job waiting on it would otherwise carry on in a worktree being removed.\n *\n * Through the shell, on purpose: a probe's command is a line from the\n * repository's own brief - `npm test -- x` - which the person approved with\n * the brief, exactly as CI runs the repository's own scripts. Nothing from\n * outside the repository reaches it.\n */\nexport function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun> {\n install();\n return new Promise((resolve) => {\n // Elsewhere the command leads a process group of its own, so the timeout\n // can stop the group. Windows has none to signal, and taskkill follows\n // the tree instead; a command detached there loses its output, which the\n // tests catch on Windows. Everywhere else `detached` is true already, so\n // the mutants that make it true are equivalent there, as are\n // `windowsHide`'s.\n const child = spawn(line, { cwd, shell: true, windowsHide: true, detached: process.platform !== 'win32', env: { ...process.env, CI: '1' } });\n running.add(child);\n let output = '';\n const keep = (chunk: Buffer): void => {\n // Enough to find a signature in; a runaway log is not kept whole.\n if (output.length < 4 * 1024 * 1024) output += chunk.toString('utf8');\n };\n // Both streams are piped, so both exist: `?.` only satisfies the type,\n // and its mutants are equivalent.\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n let stopped = false;\n const timer = setTimeout(() => {\n stopped = true;\n stop(child);\n }, timeoutSeconds * 1000);\n child.on('error', (error) => {\n // 'close' follows the error of a spawn that failed, and clears the\n // timer too, so the mutant that drops this call is equivalent.\n clearTimeout(timer);\n resolve({ exitCode: 127, output: `${output}${error.message}\\n` });\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n running.delete(child);\n if (!ending) resolve({ exitCode: stopped ? null : (code ?? 1), output });\n });\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descent-vtt/spec-harness",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "The agent-facing member of the spec-* tools: context for a round, a guard on every write, an audit when it ends, probes that prove a defect before it is filed, and rulings a person signs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@descent-vtt/spec-brief": "^0.
|
|
77
|
-
"@descent-vtt/spec-guard": "^0.
|
|
76
|
+
"@descent-vtt/spec-brief": "^0.5.1",
|
|
77
|
+
"@descent-vtt/spec-guard": "^0.19.1",
|
|
78
78
|
"@stryker-mutator/core": "^10.0.0",
|
|
79
79
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
80
80
|
"@types/node": "^22.20.4",
|
|
@@ -61,7 +61,13 @@ For a `defect` brief, write a probe: a test that fails now, for the reason
|
|
|
61
61
|
the brief states, declared in the brief as a `probe` block (with the test in
|
|
62
62
|
a `probe-file` block if it is new). Give it a `signature` - text the failure
|
|
63
63
|
must contain - or a JUnit `test` name, so a compile error is never taken for
|
|
64
|
-
the defect.
|
|
64
|
+
the defect.
|
|
65
|
+
|
|
66
|
+
The probe runs in a fresh worktree, where nothing is installed until its
|
|
67
|
+
`setup` line installs it, as `setup: npm ci` does. Make `run` one of the
|
|
68
|
+
project's own scripts, such as `npm test -- <file>`, and never `npx` with a
|
|
69
|
+
tool's name alone: where the tool is not installed, npm fetches whatever the
|
|
70
|
+
registry has under that name and runs it. Then:
|
|
65
71
|
|
|
66
72
|
```bash
|
|
67
73
|
npx --no-install @descent-vtt/spec-harness probe <id> --at base
|