@eir-labs/coltrane 0.24.32 → 0.24.34

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.
@@ -0,0 +1,32 @@
1
+ {
2
+ "slug": "release-note-verifier",
3
+ "primitives": [
4
+ "VERIFY"
5
+ ],
6
+ "input_types": [
7
+ "release-record",
8
+ "release-note"
9
+ ],
10
+ "output_types": [
11
+ "release-note-verdict"
12
+ ],
13
+ "domain": "release",
14
+ "identity": "You are the release-note-verifier. A release note is about to be published under the project's name, so you check it against the record it was written from — not against whether it reads well. You refuse a claim the record does not support, and you name a surface change the prose left out. You have no tools: the record and the note are both in front of you.",
15
+ "method": "1. For each entry in the note's `claims`: resolve its `evidence` path against the record. Absent path, or a value that disagrees with what the record holds, goes into `unsupported_claims` with what the record actually says.\n2. Read both registers for claims that are NOT in `claims` — an assertion with no evidence entry is unsupported by construction; add it with evidence 'none stated'.\n3. Walk the record's surface lists: every added or removed entry either appears in a register or goes into `missing_from_prose`.\n4. `register_faults`: plain prose carrying internal vocabulary (gig ids, chair or seat terms, costs, commit hashes), or a formal register that disagrees with the record's own fields.\n5. `pass` is true only when `unsupported_claims`, `missing_from_prose` and `register_faults` are all empty.\n6. `checks` records what you actually compared, one row per check, naming the path and the outcome.",
16
+ "constraints": [
17
+ "Ground every claim in your inputs or a tool result from this run; mark anything else as unverified rather than asserting it.",
18
+ "If your inputs are insufficient for the task, say so in the output (a caveat field or equivalent) — do not fill gaps by invention.",
19
+ "Prefer deterministic checks over reasoning; state which checks actually ran versus what was inferred.",
20
+ "Report failures verbatim (messages, counts, names), not summaries of them.",
21
+ "Judge the note against the record, never against your own sense of the release.",
22
+ "Report the record's own words verbatim when a claim disagrees with it.",
23
+ "A claim you cannot resolve to a record path is unsupported — never give it the benefit of the doubt.",
24
+ "Do not rewrite the note; state what is wrong with it."
25
+ ],
26
+ "behavioral_primitives": [
27
+ "critic",
28
+ "analyst"
29
+ ],
30
+ "allowed_tools": [],
31
+ "model_tier": "economy"
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "slug": "release-writer",
3
+ "primitives": [
4
+ "CREATE"
5
+ ],
6
+ "input_types": [
7
+ "release-record"
8
+ ],
9
+ "output_types": [
10
+ "release-note"
11
+ ],
12
+ "domain": "release",
13
+ "identity": "You are the release-writer. You are handed a release record compiled from git — the commits a tag carries, the law counts at both ends, and the surface that changed: tool slugs, tool arguments, CLI flags, environment variables, domain types. You write two registers of the same release and nothing else: a plain one for someone who USES coltrane, and a formal one for someone auditing what shipped. You have no tools and you need none: everything true about this release is in the record. What the record does not say, you do not say.",
14
+ "method": "1. Read the record. The surface lists are the substance: an added CLI flag, a new tool argument, a domain type whose version moved, a variable a deployment must now set. A surface that is null was not readable — it is not evidence of no change.\n2. `headline`: one line naming what a reader would notice. Name the thing (`--max-context-tokens`, `gig_dispatch.effort`). Never 'various improvements', never 'performance and stability'.\n3. `plain_md`: what this release means for someone using coltrane, in their vocabulary — a flag they can pass, a tool argument they can send, a variable they must set, a type whose version moved and what that obliges. Lead with what they can now do. Say plainly when a release is internal only. Use no gig ids, no costs, no chair or seat vocabulary, no commit hashes.\n4. `formal_md`: the record as a record — version, tag, date, bump, the commits by subject, the law counts at both ends, and every surface change exactly as the record states it. This register adds nothing; it restates.\n5. `claims`: every factual claim either register makes, each with the record path that supports it (`surface.cli_flags.added[1]`, `laws.after`, `commits[0].subject`) and the value read there. If you cannot name the path, the claim does not belong in the prose.\n6. `unreadable`: copy the record's `surface_unread`, and say in the plain register that those surfaces could not be read for this release rather than implying they held no change.\n7. A release whose surfaces are all empty is an internal release. Say so in one line; do not inflate it.",
15
+ "constraints": [
16
+ "Ground every claim in your inputs or a tool result from this run; mark anything else as unverified rather than asserting it.",
17
+ "If your inputs are insufficient for the task, say so in the output (a caveat field or equivalent) — do not fill gaps by invention.",
18
+ "Create only from upstream inputs and the declared task context; introduce no new external facts.",
19
+ "Where the creation makes a non-obvious choice, record the rationale alongside it.",
20
+ "Every claim in either register must appear in `claims` with the record path that supports it.",
21
+ "Never describe a change the record does not carry — no inference from a commit subject about what it did to the surface.",
22
+ "The plain register carries no internal vocabulary: no gig ids, no chair or seat terms, no costs, no commit hashes.",
23
+ "A null surface is 'could not be read', never 'nothing changed'. Say which.",
24
+ "No superlatives and no promises about future releases."
25
+ ],
26
+ "behavioral_primitives": [
27
+ "synthesizer",
28
+ "executor"
29
+ ],
30
+ "allowed_tools": [],
31
+ "model_tier": "standard"
32
+ }
package/dist/src/cli.d.ts CHANGED
@@ -34,7 +34,7 @@ export interface CliIO {
34
34
  * every other command reads process.env directly, and moving them is not this change's scope. */
35
35
  env?: Record<string, string | undefined>;
36
36
  }
37
- export declare const USAGE = "coltrane 0.24.32\n\n coltrane validate load the genome; exit non-zero on load errors OR on an\n orphan \u2014 a standards/|domain_types/|agents/ file with no\n genome_mutation seal in the tracked genome ledger\n coltrane seal-genome [<genome_dir>] bulk-seal a pre-sealing genome: append a genome_mutation\n seal for every standards/|domain_types/|agents/ file not yet\n sealed, via the blessed sealDefinition path. Content is\n byte-unchanged; a second run seals nothing already sealed.\n (default genome_dir: cwd)\n coltrane dispatch <standard> run a standard\n coltrane monitor <gig-id> report a gig's progress\n coltrane logs <gig-id> per-chair logs for a gig\n coltrane abort <gig-id> stop a running gig\n coltrane trace <output-id> walk an output's provenance\n coltrane simulate <standard> cost/shape a standard without running it\n coltrane health engine + store health\n coltrane serve run the MCP server on stdio\n coltrane enqueue <standard> queue a gig on the LOCAL file queue (no store, no key)\n (env: COLTRANE_QUEUE_DIR \u2014 its absence is the whole\n difference between local and hosted)\n coltrane work claim one queued gig from the org store and run it\n coltrane work --check report whether this box's drain environment is configured\n and exit without claiming (0 ready, 1 not ready)\n (env: COLTRANE_STORE_URL, COLTRANE_STORE_ANON,\n COLTRANE_AGENT_TOKEN; results drain via\n COLTRANE_DRAIN_URL + COLTRANE_DRAIN_KEY;\n checkpoints under COLTRANE_WORKER_CHECKPOINTS,\n default ~/.coltrane/worker-checkpoints;\n exit 0 complete or parked, 1 failed, 3 queue empty)\n coltrane reside [--any|--residency <id>] hold a residency: claim a seat, ack its channel in\n reflex, answer every wake, and drain the org's due work\n orders through its institution's governed verbs\n (env: the same contract work uses \u2014 COLTRANE_STORE_URL,\n COLTRANE_STORE_ANON, COLTRANE_SERVICE_URL, plus\n COLTRANE_DRAIN_KEY + COLTRANE_INSTANCE for venue mode;\n no new credential class\n exit 0 released, 1 cortex failed, 2 misconfigured or a\n seam no deployment wired, 3 nothing claimable)\n\nOptions\n --input <json|@file|-> dispatch payload; @file reads a file, - reads stdin\n --depth <skim|standard|deep> tighten the per-chair turn cap\n --effort <low|medium|high|xhigh|max> the reasoning effort the seat runs at\n --max-context-tokens <n> per-round context ceiling for a chat-completions seat\n --budget <dollars> per-gig ceiling; the run stops when it is gone\n --reuse allow chair-level reuse of prior sealed outputs\n --resume <gig-id> continue a gig that died mid-pipeline\n --approve <role>=<json|@file> a human chair's verdict; repeat once per chair\n --as <name> who is approving; sealed as the verdict's author\n --wait block until the run finishes\n --follow poll until the gig reaches a terminal state\n --direction <upstream|downstream|both> for trace\n --json print the raw result to stdout\n --genome <path> genome root (default: $COLTRANE_GENOME or cwd)\n --help, --version\n\nA dispatch that reaches a chair a HUMAN holds parks: it names the waiting chair and exits 0,\nbecause a gig waiting on a person is not a failed gig. Approve it on the resume \u2014\n\n coltrane dispatch <standard> --resume <gig> \\\n --approve approve=@verdict.json --as eugene\n\n--input is NOT required on an approve-only resume: when every remaining chair is human the\ncheckpoint's recorded payload stands and the omission inherits it. If you DO pass --input it is\nchecked against the checkpoint, and a disagreement still refuses the resume.\n";
37
+ export declare const USAGE = "coltrane 0.24.34\n\n coltrane validate load the genome; exit non-zero on load errors OR on an\n orphan \u2014 a standards/|domain_types/|agents/ file with no\n genome_mutation seal in the tracked genome ledger\n coltrane seal-genome [<genome_dir>] bulk-seal a pre-sealing genome: append a genome_mutation\n seal for every standards/|domain_types/|agents/ file not yet\n sealed, via the blessed sealDefinition path. Content is\n byte-unchanged; a second run seals nothing already sealed.\n (default genome_dir: cwd)\n coltrane dispatch <standard> run a standard\n coltrane monitor <gig-id> report a gig's progress\n coltrane logs <gig-id> per-chair logs for a gig\n coltrane abort <gig-id> stop a running gig\n coltrane trace <output-id> walk an output's provenance\n coltrane simulate <standard> cost/shape a standard without running it\n coltrane health engine + store health\n coltrane serve run the MCP server on stdio\n coltrane enqueue <standard> queue a gig on the LOCAL file queue (no store, no key)\n (env: COLTRANE_QUEUE_DIR \u2014 its absence is the whole\n difference between local and hosted)\n coltrane work claim one queued gig from the org store and run it\n coltrane work --check report whether this box's drain environment is configured\n and exit without claiming (0 ready, 1 not ready)\n (env: COLTRANE_STORE_URL, COLTRANE_STORE_ANON,\n COLTRANE_AGENT_TOKEN; results drain via\n COLTRANE_DRAIN_URL + COLTRANE_DRAIN_KEY;\n checkpoints under COLTRANE_WORKER_CHECKPOINTS,\n default ~/.coltrane/worker-checkpoints;\n exit 0 complete or parked, 1 failed, 3 queue empty)\n coltrane reside [--any|--residency <id>] hold a residency: claim a seat, ack its channel in\n reflex, answer every wake, and drain the org's due work\n orders through its institution's governed verbs\n (env: the same contract work uses \u2014 COLTRANE_STORE_URL,\n COLTRANE_STORE_ANON, COLTRANE_SERVICE_URL, plus\n COLTRANE_DRAIN_KEY + COLTRANE_INSTANCE for venue mode;\n no new credential class\n exit 0 released, 1 cortex failed, 2 misconfigured or a\n seam no deployment wired, 3 nothing claimable)\n\nOptions\n --input <json|@file|-> dispatch payload; @file reads a file, - reads stdin\n --depth <skim|standard|deep> tighten the per-chair turn cap\n --effort <low|medium|high|xhigh|max> the reasoning effort the seat runs at\n --max-context-tokens <n> per-round context ceiling for a chat-completions seat\n --budget <dollars> per-gig ceiling; the run stops when it is gone\n --reuse allow chair-level reuse of prior sealed outputs\n --resume <gig-id> continue a gig that died mid-pipeline\n --approve <role>=<json|@file> a human chair's verdict; repeat once per chair\n --as <name> who is approving; sealed as the verdict's author\n --wait block until the run finishes\n --follow poll until the gig reaches a terminal state\n --direction <upstream|downstream|both> for trace\n --json print the raw result to stdout\n --genome <path> genome root (default: $COLTRANE_GENOME or cwd)\n --help, --version\n\nA dispatch that reaches a chair a HUMAN holds parks: it names the waiting chair and exits 0,\nbecause a gig waiting on a person is not a failed gig. Approve it on the resume \u2014\n\n coltrane dispatch <standard> --resume <gig> \\\n --approve approve=@verdict.json --as eugene\n\n--input is NOT required on an approve-only resume: when every remaining chair is human the\ncheckpoint's recorded payload stands and the omission inherits it. If you DO pass --input it is\nchecked against the checkpoint, and a disagreement still refuses the resume.\n";
38
38
  interface Parsed {
39
39
  cmd: string | undefined;
40
40
  positional: string[];
@@ -47,3 +47,4 @@ export * from "./acoustics.js";
47
47
  export * from "./document_factory.js";
48
48
  export * from "./local_queue.js";
49
49
  export * from "./residency.js";
50
+ export * from "./releases.js";
package/dist/src/index.js CHANGED
@@ -57,4 +57,8 @@ export * from "./document_factory.js";
57
57
  // caller: "wire it or export it." Wiring is a separate act; reachability is not.
58
58
  export * from "./local_queue.js";
59
59
  export * from "./residency.js";
60
+ // The release-record compiler — reads, at each v* tag, the surface an integrator sees (MCP tools, CLI
61
+ // flags, worker env, domain-type versions, law counts) and reports what each release changed. A public
62
+ // entrypoint, exported here so the public-entrypoint rule can name it.
63
+ export * from "./releases.js";
60
64
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,6FAA6F;AAC7F,iGAAiG;AACjG,qFAAqF;AACrF,2FAA2F;AAC3F,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AAEtC,8FAA8F;AAC9F,mGAAmG;AACnG,8FAA8F;AAC9F,oGAAoG;AACpG,+FAA+F;AAC/F,iFAAiF;AACjF,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,6FAA6F;AAC7F,iGAAiG;AACjG,qFAAqF;AACrF,2FAA2F;AAC3F,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AAEtC,8FAA8F;AAC9F,mGAAmG;AACnG,8FAA8F;AAC9F,oGAAoG;AACpG,+FAA+F;AAC/F,iFAAiF;AACjF,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAE/B,sGAAsG;AACtG,uGAAuG;AACvG,uEAAuE;AACvE,cAAc,eAAe,CAAC"}
@@ -0,0 +1,107 @@
1
+ /** One commit in a release's range: its full sha and its subject line. */
2
+ export interface Commit {
3
+ sha: string;
4
+ subject: string;
5
+ }
6
+ /** A surface delta against the predecessor tag: both lists sorted, plain strings. `null` when the
7
+ * file the list is read from was absent or unparseable AT THE TAG — distinguishable from an empty
8
+ * list, which means "present and nothing changed". */
9
+ export type SurfaceList = {
10
+ added: string[];
11
+ removed: string[];
12
+ } | null;
13
+ /** The surface an integrator sees, keyed by what it is read from. */
14
+ export interface ReleaseSurface {
15
+ mcp_tools: SurfaceList;
16
+ mcp_tool_args: SurfaceList;
17
+ cli_flags: SurfaceList;
18
+ env_vars: SurfaceList;
19
+ domain_types: SurfaceList;
20
+ }
21
+ /** One release, compiled from git at a v* tag. */
22
+ export interface ReleaseRecord {
23
+ /** The semver — the tag without its `v`. */
24
+ version: string;
25
+ /** The ref. */
26
+ tag: string;
27
+ /** The tag's commit date, ISO. */
28
+ date: string;
29
+ /** The range since the previous tag, oldest first; the earliest tag's range is every commit up to it. */
30
+ commits: Commit[];
31
+ /** Derived the scripts/next_version.mjs way from the range's subjects and trailers, never the tag numbers. */
32
+ bump: "major" | "minor" | "patch";
33
+ /** Law/file counts read from scripts/laws.sh: `after` at this tag, `before` the predecessor's `after`
34
+ * (`null` for the earliest tag — there is no predecessor count, and `0` would falsely claim an
35
+ * empty suite the day before). */
36
+ laws: {
37
+ before: number | null;
38
+ after: number | null;
39
+ files: number | null;
40
+ };
41
+ surface: ReleaseSurface;
42
+ /** The surface keys that were PRESENT at the tag but yielded no readable entry, sorted. A surface
43
+ * whose FILE is absent is NOT unread — it is simply absent, and its list is `null` for that reason;
44
+ * only a file that was there and could not be parsed is named here. Always present, empty when
45
+ * every present surface read. Named so a changelog can say "could not read the tool registry here"
46
+ * instead of showing a silent gap that reads as "nothing changed". */
47
+ surface_unread: string[];
48
+ /** `false` for a tagged release; `true` for the one PENDING record `compileReleases({ pending })`
49
+ * appends — the version about to be published, compiled from HEAD before its tag is pushed. The
50
+ * field is always present so a consumer never has to guess whether a record is settled. */
51
+ pending: boolean;
52
+ }
53
+ /** One claim in a release note, and the evidence path that backs it. `value` is the string a writing
54
+ * seat recorded as "the value read at that path"; the renderer resolves `evidence` against the record
55
+ * and marks the row UNSUPPORTED when the path is absent or holds a different value. */
56
+ export interface ReleaseNoteClaim {
57
+ text: string;
58
+ evidence: string;
59
+ value?: string;
60
+ }
61
+ /** A release note (release-notes-v0): two registers of prose, the claims that back the headline, and
62
+ * the surfaces a compiler could not read. The renderer turns this into markdown; it never presents a
63
+ * claim the record does not support. */
64
+ export interface ReleaseNote {
65
+ version: string;
66
+ headline: string;
67
+ plain_md: string;
68
+ formal_md: string;
69
+ claims: ReleaseNoteClaim[];
70
+ unreadable?: string[];
71
+ }
72
+ /**
73
+ * Compile a ReleaseRecord per v* tag, oldest first, reading every field AT the tag. Throws — never
74
+ * returns `[]` — when `tree_root` is not a git repository, or is one that holds no v* tag: an empty
75
+ * list would read as "this project has no releases", a different and false claim from "there is no
76
+ * repository to read" or "there are no tags yet".
77
+ *
78
+ * With `pending: { version }`, ONE further record is appended for the commits since the last tag,
79
+ * compiled through the same path as a tagged release (tag = `v<version>`, date/commits/laws/surface
80
+ * read from HEAD against the last tag) and marked `pending: true`. It is the record the tag WOULD
81
+ * carry once cut — the package can then ship the history it is itself part of. A pending version a tag
82
+ * already holds is refused by name, rather than emit two records for one version.
83
+ */
84
+ export declare function compileReleases(opts: {
85
+ tree_root: string;
86
+ pending?: {
87
+ version: string;
88
+ };
89
+ }): ReleaseRecord[];
90
+ /** The whole release history as a stable JSON document, NEWEST first — the file the package ships so
91
+ * the changelog UI reads a settled artifact rather than shelling out to git. Pretty-printed (2-space)
92
+ * with a single trailing newline, byte-identical for the same inputs. Same options as
93
+ * `compileReleases`, so a pending version leads the document. */
94
+ export declare function releasesJson(options: {
95
+ tree_root: string;
96
+ pending?: {
97
+ version: string;
98
+ };
99
+ }): string;
100
+ /** Render one release note to markdown: the headline as a heading, then the plain register, then the
101
+ * formal register, then an evidence section with one row per claim. Every field the note declares is
102
+ * rendered; a surface named in `unreadable` is STATED as unreadable, never omitted. A claim whose
103
+ * `evidence` path does not resolve in the record, or resolves to a different value than its `value`,
104
+ * is marked UNSUPPORTED naming what the record actually holds — it is never dropped and never shown
105
+ * as if evidenced. The rendering is derived from the NOTE: no evidence row appears that the note did
106
+ * not claim (I2). */
107
+ export declare function renderReleaseNote(note: ReleaseNote, record: ReleaseRecord): string;
@@ -0,0 +1,414 @@
1
+ // THE RELEASE RECORD, COMPILED FROM GIT (contract-release-record-v1).
2
+ //
3
+ // The measured problem: `.github/workflows/publish.yml` cuts a release on every push to main and
4
+ // derives the version from conventional-commit prefixes. Across v0.24.20..v0.24.31, 0 of 11 subjects
5
+ // carried a prefix, so every tag shipped as a patch — including one that added a domain-type version,
6
+ // two CLI flags, a gig_dispatch argument and three refusal kinds. A changelog grouped by the bump
7
+ // renders one flat list, and the bump cannot be trusted to say what changed.
8
+ //
9
+ // So the repo answers the question MECHANICALLY. Every surface an integrator sees is data readable at
10
+ // any tag with `git show <tag>:<path>`: the MCP tool registry (src/mcp.ts), the CLI help block
11
+ // (src/cli.ts), the worker env table (src/worker_env.ts), each domain type's version
12
+ // (domain_types/*.json) and the law/file counts (scripts/laws.sh). `compileReleases({ tree_root })`
13
+ // reads those at each v* tag and reports a ReleaseRecord per tag: derived, deterministic, refusing
14
+ // rather than guessing.
15
+ //
16
+ // Every git read goes through execFileSync("git", ["-C", tree_root, ...]) — no network, no shell
17
+ // string, no process.cwd(), and everything is read AT THE TAG so a record compiled today equals the
18
+ // record compiled the day the tag was cut. The parse is regex over the text at that revision, in the
19
+ // style of src/repo_index.ts (no AST): an old tag's module may not compile against today's types, so
20
+ // the surface is text at that revision, never an import of it.
21
+ import { execFileSync } from "node:child_process";
22
+ // ── the conventional-commit bump rule, byte-for-byte scripts/next_version.mjs ────────────────────────
23
+ const BREAKING = /^[a-zA-Z]+(\([^)]*\))?!:/;
24
+ const BREAKING_TRAILER = /^BREAKING[ -]CHANGE:/;
25
+ const FEAT = /^feat(\([^)]*\))?:/;
26
+ /** Run git under `tree_root`, capturing stdout. Throws on a non-zero exit (an absent path, a bad ref),
27
+ * which the surface readers catch and turn into `null`. stderr is discarded so a missing file does not
28
+ * spam the caller's console. */
29
+ function gitCapture(root, args) {
30
+ return execFileSync("git", ["-C", root, ...args], {
31
+ encoding: "utf8",
32
+ stdio: ["ignore", "pipe", "ignore"],
33
+ maxBuffer: 64 * 1024 * 1024,
34
+ });
35
+ }
36
+ /** The text of `<path>` AT `<tag>`, or `null` when it does not exist there. */
37
+ function showAtTag(root, tag, path) {
38
+ try {
39
+ return gitCapture(root, ["show", `${tag}:${path}`]);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /** The MCP tool slugs and their advertised argument names from src/mcp.ts. Read from the tool
46
+ * ENTRIES themselves — any `{ slug: "<name>" … input_schema: obj({ … }) }` object — so the array's
47
+ * NAME, its type annotation and any `.map()` export are irrelevant: the real registry is a typed
48
+ * `TOOL_DEFS` array exported through `.map()`, and the simplified control fixture is
49
+ * `export const MCP_TOOLS = [`, and both parse the same way. `null` when no such entry can be read
50
+ * (the file is absent, or present but not a tool registry). Args are `"<slug>.<argument>"`. */
51
+ function parseMcp(content) {
52
+ if (content === null)
53
+ return null;
54
+ const tools = new Set();
55
+ const args = new Set();
56
+ const toolRe = /slug:\s*"([^"]+)"[\s\S]*?input_schema:\s*obj\(\{([\s\S]*?)\}\)/g;
57
+ let m;
58
+ while ((m = toolRe.exec(content)) !== null) {
59
+ const slug = m[1];
60
+ tools.add(slug);
61
+ // TOP-LEVEL keys only. A nested object argument (`budget: { type: "object", properties: { … } }`)
62
+ // carries JSON-Schema keywords, and a flat scan reports `type` and `properties` as arguments the
63
+ // tool accepts — measured on this repo's own gig_dispatch. Walk the body tracking brace depth and
64
+ // take a key only at depth 0; the nested argument is still named, by its own key.
65
+ const body = m[2];
66
+ let depth = 0;
67
+ let key = "";
68
+ for (let i = 0; i < body.length; i++) {
69
+ const ch = body[i];
70
+ if (ch === "{" || ch === "(" || ch === "[") {
71
+ depth++;
72
+ key = "";
73
+ continue;
74
+ }
75
+ if (ch === "}" || ch === ")" || ch === "]") {
76
+ depth = Math.max(0, depth - 1);
77
+ key = "";
78
+ continue;
79
+ }
80
+ if (depth > 0)
81
+ continue;
82
+ if (/[A-Za-z0-9_$]/.test(ch)) {
83
+ key += ch;
84
+ continue;
85
+ }
86
+ if (ch === ":" && key.length > 0)
87
+ args.add(`${slug}.${key}`);
88
+ if (!/\s/.test(ch) || ch === ",")
89
+ key = "";
90
+ if (/\s/.test(ch) && key.length > 0)
91
+ continue;
92
+ }
93
+ }
94
+ // No tool ENTRY could be read — the file is present but not a tool registry. `null`, so the record
95
+ // NAMES it unread rather than passing an empty tool set off as "nothing changed".
96
+ if (tools.size === 0)
97
+ return null;
98
+ return { tools, args };
99
+ }
100
+ /** The long `--flags` from src/cli.ts. A flag is any line whose FIRST non-space characters are
101
+ * `--<name>`, anywhere in the file; the leading token is taken WITHOUT its `<placeholder>`
102
+ * (`--effort <low|…>` → `--effort`). No `HELP`/`USAGE` constant is required — the real block is a
103
+ * `USAGE` template literal — and a `--` token that is NOT the start of its line (`--check` inside a
104
+ * `coltrane …` usage example) is not a flag. `null` when no flag line can be read. */
105
+ function parseCli(content) {
106
+ if (content === null)
107
+ return null;
108
+ const flags = new Set();
109
+ const lineRe = /^[ \t]*(--[a-z][a-z0-9-]*)/;
110
+ for (const line of content.split("\n")) {
111
+ const m = lineRe.exec(line);
112
+ if (m)
113
+ flags.add(m[1]);
114
+ }
115
+ if (flags.size === 0)
116
+ return null;
117
+ return flags;
118
+ }
119
+ /** The variable names from the worker environment contract in src/worker_env.ts, read from the
120
+ * `name: "…"` values of the ENTRIES — not from the bare identifier. A file that only MENTIONS
121
+ * WORKER_ENV_CONTRACT (a comment, a type whose `name: string;` field carries no quotes) but declares
122
+ * no entry array yields nothing, so it reads `null` (present-but-unread, NAMED by the record) rather
123
+ * than an empty set that would render the predecessor's whole env table as removed. `null` also when
124
+ * the file is absent. */
125
+ function parseEnv(content) {
126
+ if (content === null)
127
+ return null;
128
+ const names = new Set();
129
+ const re = /name:\s*"([^"]+)"/g;
130
+ let m;
131
+ while ((m = re.exec(content)) !== null)
132
+ names.add(m[1]);
133
+ if (names.size === 0)
134
+ return null;
135
+ return names;
136
+ }
137
+ /** EXPECTED_LAWS and EXPECTED_FILES from scripts/laws.sh, each `null` when the count is absent. */
138
+ function parseLaws(content) {
139
+ if (content === null)
140
+ return { laws: null, files: null };
141
+ const lm = /EXPECTED_LAWS="\$\{EXPECTED_LAWS:-(\d+)\}"/.exec(content);
142
+ const fm = /EXPECTED_FILES="\$\{EXPECTED_FILES:-(\d+)\}"/.exec(content);
143
+ return { laws: lm ? Number(lm[1]) : null, files: fm ? Number(fm[1]) : null };
144
+ }
145
+ /** `"<slug>@<version>"` per file under domain_types/ AT `<tag>`. `null` when no domain_types directory
146
+ * exists at the tag; an individual unparseable file contributes nothing rather than nulling the set. */
147
+ function parseDomainTypes(root, tag) {
148
+ let listing;
149
+ try {
150
+ listing = gitCapture(root, ["ls-tree", "-r", "--name-only", tag, "domain_types"]);
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ const files = listing.split("\n").filter((p) => p.endsWith(".json"));
156
+ if (files.length === 0)
157
+ return null;
158
+ const set = new Set();
159
+ for (const file of files) {
160
+ const content = showAtTag(root, tag, file);
161
+ if (content === null)
162
+ continue;
163
+ try {
164
+ const j = JSON.parse(content);
165
+ if (j.slug !== undefined && j.version !== undefined)
166
+ set.add(`${j.slug}@${j.version}`);
167
+ }
168
+ catch {
169
+ /* an unparseable domain-type file contributes nothing */
170
+ }
171
+ }
172
+ return set;
173
+ }
174
+ /** The surface delta: everything in `after` not in `before` is added, and vice versa, both sorted.
175
+ * `null` when the file is absent/unparseable AT THE TAG (`after` is `null`); a `before` that is
176
+ * absent (an older release predating the file, or no predecessor at all) counts as the empty set, so
177
+ * the tag that introduced a file reports its whole contents as `added`, never `null`. */
178
+ function diffSurface(after, before) {
179
+ if (after === null)
180
+ return null;
181
+ const prev = before ?? new Set();
182
+ const added = [...after].filter((x) => !prev.has(x)).sort();
183
+ const removed = [...prev].filter((x) => !after.has(x)).sort();
184
+ return { added, removed };
185
+ }
186
+ /** The commits in `prev..tag`, oldest first; the earliest tag's range is every commit up to it. */
187
+ function rangeCommits(root, prevTag, tag) {
188
+ const range = prevTag ? `${prevTag}..${tag}` : tag;
189
+ const out = gitCapture(root, ["log", "--reverse", "--format=%H%x09%s", range]);
190
+ return out
191
+ .split("\n")
192
+ .filter(Boolean)
193
+ .map((line) => {
194
+ const tab = line.indexOf("\t");
195
+ return { sha: line.slice(0, tab), subject: line.slice(tab + 1) };
196
+ });
197
+ }
198
+ /** The bump for `prev..tag`, derived the scripts/next_version.mjs way over the range's full commit
199
+ * bodies (subjects and trailers): a `!:` prefix or a BREAKING-CHANGE trailer is major, a feat: is
200
+ * minor, everything else is patch. Highest bump wins. */
201
+ function rangeBump(root, prevTag, tag) {
202
+ const range = prevTag ? `${prevTag}..${tag}` : tag;
203
+ const body = gitCapture(root, ["log", "--format=%B", range]);
204
+ const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
205
+ let bump = "patch";
206
+ for (const line of lines) {
207
+ if (BREAKING.test(line) || BREAKING_TRAILER.test(line))
208
+ return "major";
209
+ if (FEAT.test(line))
210
+ bump = "minor";
211
+ }
212
+ return bump;
213
+ }
214
+ /** Read and parse every surface at `tag` with the minimum number of git reads — one `git show` per
215
+ * text surface, one `ls-tree` (+ a show per domain-type file) for domain_types, one for laws.sh. */
216
+ function surfacesAtTag(root, tag) {
217
+ const mcpContent = showAtTag(root, tag, "src/mcp.ts");
218
+ const cliContent = showAtTag(root, tag, "src/cli.ts");
219
+ const envContent = showAtTag(root, tag, "src/worker_env.ts");
220
+ return {
221
+ mcp: parseMcp(mcpContent),
222
+ mcpPresent: mcpContent !== null,
223
+ cli: parseCli(cliContent),
224
+ cliPresent: cliContent !== null,
225
+ env: parseEnv(envContent),
226
+ envPresent: envContent !== null,
227
+ domainTypes: parseDomainTypes(root, tag),
228
+ laws: parseLaws(showAtTag(root, tag, "scripts/laws.sh")),
229
+ };
230
+ }
231
+ /** Assemble one record from an already-parsed surface bundle. `ref` is the git ref the date and the
232
+ * commit range END are read from — a tag for a settled release, `HEAD` for the pending one — and
233
+ * `rangeStart` is the predecessor the range and the surface diff run against (`null` for the earliest
234
+ * release, which reports its whole surface as added). Pulling this out of the loop lets the pending
235
+ * record be compiled through EXACTLY the same path as a tagged one, so the two can only differ in the
236
+ * `pending` flag (I1). `laws.before` is filled by the caller's chaining pass. */
237
+ function assembleRecord(root, meta, after, before) {
238
+ // F1 — a surface whose FILE was present but yielded nothing readable is NAMED, so a bare `null` is
239
+ // never mistaken for "nothing changed"; an absent file is not named. mcp.ts backs two surface keys.
240
+ const surfaceUnread = [];
241
+ if (after.mcpPresent && after.mcp === null)
242
+ surfaceUnread.push("mcp_tools", "mcp_tool_args");
243
+ if (after.cliPresent && after.cli === null)
244
+ surfaceUnread.push("cli_flags");
245
+ if (after.envPresent && after.env === null)
246
+ surfaceUnread.push("env_vars");
247
+ surfaceUnread.sort();
248
+ return {
249
+ version: meta.version,
250
+ tag: meta.tag,
251
+ date: gitCapture(root, ["log", "-1", "--format=%cI", meta.ref]).trim(),
252
+ commits: rangeCommits(root, meta.rangeStart, meta.ref),
253
+ bump: rangeBump(root, meta.rangeStart, meta.ref),
254
+ laws: { before: null, after: after.laws.laws, files: after.laws.files },
255
+ surface: {
256
+ mcp_tools: diffSurface(after.mcp ? after.mcp.tools : null, before?.mcp ? before.mcp.tools : null),
257
+ mcp_tool_args: diffSurface(after.mcp ? after.mcp.args : null, before?.mcp ? before.mcp.args : null),
258
+ cli_flags: diffSurface(after.cli, before?.cli ?? null),
259
+ env_vars: diffSurface(after.env, before?.env ?? null),
260
+ domain_types: diffSurface(after.domainTypes, before?.domainTypes ?? null),
261
+ },
262
+ surface_unread: surfaceUnread,
263
+ pending: meta.pending,
264
+ };
265
+ }
266
+ /**
267
+ * Compile a ReleaseRecord per v* tag, oldest first, reading every field AT the tag. Throws — never
268
+ * returns `[]` — when `tree_root` is not a git repository, or is one that holds no v* tag: an empty
269
+ * list would read as "this project has no releases", a different and false claim from "there is no
270
+ * repository to read" or "there are no tags yet".
271
+ *
272
+ * With `pending: { version }`, ONE further record is appended for the commits since the last tag,
273
+ * compiled through the same path as a tagged release (tag = `v<version>`, date/commits/laws/surface
274
+ * read from HEAD against the last tag) and marked `pending: true`. It is the record the tag WOULD
275
+ * carry once cut — the package can then ship the history it is itself part of. A pending version a tag
276
+ * already holds is refused by name, rather than emit two records for one version.
277
+ */
278
+ export function compileReleases(opts) {
279
+ const root = opts.tree_root;
280
+ // F2 — a tree_root that is not a git repository. Refuse by name rather than return [].
281
+ try {
282
+ execFileSync("git", ["-C", root, "rev-parse", "--git-dir"], {
283
+ stdio: ["ignore", "ignore", "ignore"],
284
+ });
285
+ }
286
+ catch {
287
+ throw new Error(`compileReleases: "${root}" is not a git repository — no releases to compile`);
288
+ }
289
+ // Tags in version order, oldest first.
290
+ const tags = gitCapture(root, ["tag", "--list", "v*", "--sort=version:refname"])
291
+ .split("\n")
292
+ .filter(Boolean);
293
+ // F2 — a git repository with no v* tag. Refuse by name rather than return [].
294
+ if (tags.length === 0) {
295
+ throw new Error(`compileReleases: "${root}" is a git repository but holds no v* tags — there are no releases to compile`);
296
+ }
297
+ // F1 (history) — a pending version a tag already holds would be two records for one version. Refuse
298
+ // by name, before any compilation, naming both the version and the tag that already holds it.
299
+ const pendingTag = opts.pending ? `v${opts.pending.version}` : null;
300
+ if (opts.pending && pendingTag !== null && tags.includes(pendingTag)) {
301
+ throw new Error(`compileReleases: pending version ${opts.pending.version} is already released as tag ${pendingTag} — refusing to emit a second record for one version`);
302
+ }
303
+ const records = [];
304
+ let before = null;
305
+ for (let i = 0; i < tags.length; i++) {
306
+ const tag = tags[i];
307
+ const prevTag = i > 0 ? tags[i - 1] : null;
308
+ // Parse every surface at this tag ONCE; the previous iteration's bundle is this tag's "before",
309
+ // so no tag's content is fetched twice. Presence and readability are kept apart inside assemble.
310
+ const after = surfacesAtTag(root, tag);
311
+ records.push(assembleRecord(root, { version: tag.replace(/^v/, ""), tag, ref: tag, rangeStart: prevTag, pending: false }, after, before));
312
+ before = after;
313
+ }
314
+ // The pending release: HEAD compiled as if it were the tag `v<version>`, diffed against the last
315
+ // tag (which `before` still holds), marked pending. Same path as a tagged record, so it equals the
316
+ // record that tag would carry save the flag.
317
+ if (opts.pending && pendingTag !== null) {
318
+ const lastTag = tags[tags.length - 1];
319
+ const headSurfaces = surfacesAtTag(root, "HEAD");
320
+ records.push(assembleRecord(root, { version: opts.pending.version, tag: pendingTag, ref: "HEAD", rangeStart: lastTag, pending: true }, headSurfaces, before));
321
+ }
322
+ // laws.before chains from the predecessor's after; the earliest release keeps null.
323
+ for (let i = 1; i < records.length; i++) {
324
+ records[i].laws.before = records[i - 1].laws.after;
325
+ }
326
+ return records;
327
+ }
328
+ /** The whole release history as a stable JSON document, NEWEST first — the file the package ships so
329
+ * the changelog UI reads a settled artifact rather than shelling out to git. Pretty-printed (2-space)
330
+ * with a single trailing newline, byte-identical for the same inputs. Same options as
331
+ * `compileReleases`, so a pending version leads the document. */
332
+ export function releasesJson(options) {
333
+ const releases = compileReleases(options).reverse();
334
+ return JSON.stringify({ generated_from: "compileReleases", releases }, null, 2) + "\n";
335
+ }
336
+ /** Resolve a dotted/indexed evidence path (`laws.after`, `surface.cli_flags.added[0]`) against a
337
+ * record. Returns whether the path resolved and the value it reached — the plain way: a segment is a
338
+ * key, and any `[n]` on it is an array index applied in order. A key missing on an object, or an
339
+ * index off the end of an array, does not resolve. */
340
+ function resolveEvidencePath(record, path) {
341
+ let cur = record;
342
+ for (const segment of path.split(".")) {
343
+ const keyMatch = /^[^[\]]*/.exec(segment);
344
+ const key = keyMatch ? keyMatch[0] : "";
345
+ const steps = [];
346
+ if (key !== "")
347
+ steps.push(key);
348
+ const idxRe = /\[(\d+)\]/g;
349
+ let idx;
350
+ while ((idx = idxRe.exec(segment)) !== null)
351
+ steps.push(Number(idx[1]));
352
+ for (const step of steps) {
353
+ if (cur === null || cur === undefined)
354
+ return { resolved: false, value: undefined };
355
+ if (typeof step === "number") {
356
+ if (!Array.isArray(cur) || step >= cur.length)
357
+ return { resolved: false, value: undefined };
358
+ cur = cur[step];
359
+ }
360
+ else {
361
+ if (typeof cur !== "object" || cur === null || !(step in cur)) {
362
+ return { resolved: false, value: undefined };
363
+ }
364
+ cur = cur[step];
365
+ }
366
+ }
367
+ }
368
+ return { resolved: true, value: cur };
369
+ }
370
+ /** The value at an evidence path, as the string a claim's `value` is compared against: a string is
371
+ * itself, anything else is JSON-stringified so `150` reads as `"150"` and an array reads as its JSON. */
372
+ function evidenceValueString(value) {
373
+ return typeof value === "string" ? value : JSON.stringify(value);
374
+ }
375
+ /** Render one release note to markdown: the headline as a heading, then the plain register, then the
376
+ * formal register, then an evidence section with one row per claim. Every field the note declares is
377
+ * rendered; a surface named in `unreadable` is STATED as unreadable, never omitted. A claim whose
378
+ * `evidence` path does not resolve in the record, or resolves to a different value than its `value`,
379
+ * is marked UNSUPPORTED naming what the record actually holds — it is never dropped and never shown
380
+ * as if evidenced. The rendering is derived from the NOTE: no evidence row appears that the note did
381
+ * not claim (I2). */
382
+ export function renderReleaseNote(note, record) {
383
+ const lines = [];
384
+ lines.push(`# ${note.headline}`, "");
385
+ lines.push(note.plain_md, "");
386
+ lines.push(note.formal_md, "");
387
+ lines.push("## Evidence", "");
388
+ for (const claim of note.claims) {
389
+ const { resolved, value } = resolveEvidencePath(record, claim.evidence);
390
+ const held = resolved ? evidenceValueString(value) : null;
391
+ const claimed = claim.value;
392
+ // Supported iff the path resolved AND (no value was claimed, or the claimed value matches what the
393
+ // record holds). Otherwise the row is marked UNSUPPORTED naming what the record actually holds.
394
+ const supported = resolved && (claimed === undefined || held === claimed);
395
+ if (supported) {
396
+ lines.push(`- ${claim.text} — \`${claim.evidence}\` → \`${held ?? ""}\``);
397
+ }
398
+ else if (!resolved) {
399
+ lines.push(`- ${claim.text} — \`${claim.evidence}\` → UNSUPPORTED (the path does not resolve in the record)`);
400
+ }
401
+ else {
402
+ lines.push(`- ${claim.text} — \`${claim.evidence}\` → UNSUPPORTED (the record holds \`${held}\`)`);
403
+ }
404
+ }
405
+ const unreadable = note.unreadable ?? [];
406
+ if (unreadable.length > 0) {
407
+ lines.push("", "## Unreadable surfaces", "");
408
+ for (const surface of unreadable) {
409
+ lines.push(`- \`${surface}\` could not be read at this release`);
410
+ }
411
+ }
412
+ return lines.join("\n") + "\n";
413
+ }
414
+ //# sourceMappingURL=releases.js.map