@popovych.co/witness 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/abandon.js CHANGED
@@ -7,7 +7,7 @@ import { canonicalSha } from './sha.js';
7
7
  import { gitBlob, tryGit, stateCommit } from './gitio.js';
8
8
  import { acquireLock } from './lock.js';
9
9
  import { crashPoint, withTxn } from './txn.js';
10
- import { appendEntry, effortAbandoned, effortStreams, entryLine, journalRel, readStream } from './journal.js';
10
+ import { appendEntry, effortAbandoned, effortStreams, entryLine, journalActs, journalRel, readStream } from './journal.js';
11
11
  import { findById, plansOn } from './scan.js';
12
12
  import { humanCall, ok, refuse, v } from './refusal.js';
13
13
  import { prepareStamp, writeStamp } from './stamp.js';
@@ -18,25 +18,15 @@ const isWrite = (e, id, sha) => {
18
18
  return w.t === 'write' && w.artifact === id && (sha === undefined || w.sha === sha);
19
19
  };
20
20
  const isAncestor = (root, a, b) => tryGit(root, 'merge-base', '--is-ancestor', a, b).ok;
21
- // D168. Each line of a stream as history stood at `at`, with the commit that appended it — which
22
- // `git blame` names because the journal is append-only. `--no-ignore-revs-file`: a blame config
23
- // (a dotfile's `blame.ignoreRevsFile`) re-attributes lines, or makes blame fail outright in a repo
24
- // that lacks the file. A blank line is skipped, as `readStream` skips it.
25
- function journalActs(root, stream, at = 'HEAD') {
26
- const blame = tryGit(root, 'blame', '--line-porcelain', '--no-ignore-revs-file', at, '--', journalRel(stream));
27
- if (!blame.ok)
28
- return [];
29
- const out = [];
30
- let commit = '';
31
- for (const line of blame.out.split('\n')) {
32
- if (/^[0-9a-f]{40} \d+ \d+/.test(line)) {
33
- commit = line.slice(0, 40);
34
- continue;
35
- }
36
- if (line.startsWith('\t') && line.trim() !== '')
37
- out.push({ entry: JSON.parse(line.slice(1)), commit });
38
- }
39
- return out;
21
+ // D189. Of two commits, the later: by ancestry, and on parallel branches — where neither is an
22
+ // ancestor, as after a merge of two clones' journals — by commit time.
23
+ function later(root, a, b) {
24
+ if (isAncestor(root, a, b))
25
+ return b;
26
+ if (isAncestor(root, b, a))
27
+ return a;
28
+ const time = (c) => Number(tryGit(root, 'log', '-1', '--format=%ct', c).out) || 0;
29
+ return time(b) > time(a) ? b : a;
40
30
  }
41
31
  // D168. The act this content of a spec is, as history stood at `at`: the newest write of it, by
42
32
  // whichever effort made it. Rows 29 and 84 resolved a pin by walking the spec's path for matching
@@ -53,7 +43,7 @@ export function writeAct(root, id, sha, at = 'HEAD') {
53
43
  for (const { entry, commit } of journalActs(root, effort, at)) {
54
44
  if (!isWrite(entry, id, sha))
55
45
  continue;
56
- if (found === undefined || isAncestor(root, found.commit, commit)) {
46
+ if (found === undefined || later(root, found.commit, commit) === commit) {
57
47
  found = { effort, commit, created: entry.created === true };
58
48
  }
59
49
  }
@@ -69,11 +59,11 @@ const shaAt = (root, rev, rel) => {
69
59
  // stood on it or on a sibling slice, or the walk stopped at it. That content is canon of record:
70
60
  // no later walk goes past it, or an amendment on top of it took the keep with it.
71
61
  function stoodAtAbandon(root, effort, rel, sha) {
72
- const at = journalActs(root, effort).find(({ entry }) => entry.t === 'human-decision' && entry.decision === 'abandon-effort');
62
+ const at = journalActs(root, effort, 'HEAD').find(({ entry }) => entry.t === 'human-decision' && entry.decision === 'abandon-effort');
73
63
  return at !== undefined && shaAt(root, at.commit, rel) === sha;
74
64
  }
75
65
  // This effort's newest write of an artifact (of this content, when `sha` is given).
76
- export const effortWrite = (root, effort, id, sha) => journalActs(root, effort).filter(({ entry }) => isWrite(entry, id, sha)).at(-1);
66
+ export const effortWrite = (root, effort, id, sha) => journalActs(root, effort, 'HEAD').filter(({ entry }) => isWrite(entry, id, sha)).at(-1);
77
67
  // D168 (review). Whether an earlier abandon already took this effort's newest write of a spec
78
68
  // back — `abandon <plan>` does, before the effort's own abandon: a status entry the abandon
79
69
  // journaled on the spec after that write, which is not a return, made where the spec stood at
@@ -82,7 +72,7 @@ export function takenBackAlready(root, effort, id, rel, sha) {
82
72
  const write = effortWrite(root, effort, id, sha);
83
73
  if (write === undefined)
84
74
  return false;
85
- return journalActs(root, id).some(({ entry, commit }) => {
75
+ return journalActs(root, id, 'HEAD').some(({ entry, commit }) => {
86
76
  const s = entry;
87
77
  const takes = s.t === 'status' && s.cause === 'abandon' && !(s.from === 'superseded' && s.to !== 'removed');
88
78
  return takes && commit !== write.commit && isAncestor(root, write.commit, commit) && shaAt(root, `${commit}^`, rel) === sha;
@@ -177,11 +167,11 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, act, effort) {
177
167
  // takes the split back together; deleting the slice now left the split half described. The
178
168
  // effort path is that last act, and there a slice another effort cut cannot go back together
179
169
  // with this one: holding it would leave two efforts' abandons each refusing for the other's.
180
- const from = spec.meta.supersedes === undefined ? undefined : String(spec.meta.supersedes);
181
- const slice = effort === undefined && back.end === 'create' && from !== undefined && canonExit(readStream(root, from))?.to === 'superseded' ? [from] : [];
170
+ const own = spec.meta.supersedes === undefined ? undefined : String(spec.meta.supersedes);
171
+ const slice = effort === undefined && back.end === 'create' && own !== undefined && canonExit(readStream(root, own))?.to === 'superseded' ? [own] : [];
182
172
  const cut = new Set([...returns.map((r) => r.entry.artifact), ...unwound.map((u) => u.artifact), ...slice]);
183
173
  const slices = canon.docs
184
- .filter((d) => d.meta.type === 'spec' && cut.has(String(d.meta.supersedes)) && String(d.meta.id) !== String(spec.meta.id))
174
+ .filter((d) => d.meta.type === 'spec' && cut.has(String(d.meta.supersedes)) && String(d.meta.id) !== id)
185
175
  .flatMap((d) => {
186
176
  const id = String(d.meta.id);
187
177
  const leaving = abandonSet.has(id);
@@ -59,7 +59,8 @@ registerGate({
59
59
  detail: staleChildren.length
60
60
  ? `amended under in-progress plans (approve = explicit ack): ${staleChildren.join(' ')}`
61
61
  : '' });
62
- const created = [...writes.values()].some((w) => w.created === true);
62
+ // D189. A spec the effort created — a plan it created is no misroute.
63
+ const created = docs.some((d) => writes.get(String(d.meta.id))?.created === true);
63
64
  const standingStop = recap.class === 'feature' ? 'feature scope approval — scope is approved as its concrete slicing'
64
65
  : recap.class === 'fix' && created
65
66
  ? 'fix created a new spec — misroute tripwire (young canon: expect new-spec stops at first)'
package/dist/journal.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { tryGit } from './gitio.js';
3
4
  import { version } from './version.js';
4
5
  const dir = (root) => join(root, '.witness', 'journal');
5
6
  export const journalRel = (id) => `.witness/journal/${id}.jsonl`;
@@ -27,6 +28,28 @@ export function readStream(root, id) {
27
28
  .filter(Boolean)
28
29
  .map((l) => JSON.parse(l));
29
30
  }
31
+ // D168, D189. Each line of a stream with the commit that appended it — `git blame` names it,
32
+ // since the journal is append-only — as history stood at `at`, or as the file stands now when
33
+ // `at` is absent (a line not yet committed carries git's all-zero id). `--no-ignore-revs-file`: a
34
+ // dotfile's `blame.ignoreRevsFile` re-attributes lines, or fails blame outright in a repo that
35
+ // lacks the file. A blank line is skipped, as `readStream` skips it. SHA-1 or SHA-256 ids.
36
+ export function journalActs(root, stream, at) {
37
+ const blame = tryGit(root, 'blame', '--line-porcelain', '--no-ignore-revs-file', ...(at === undefined ? [] : [at]), '--', journalRel(stream));
38
+ if (!blame.ok)
39
+ return [];
40
+ const out = [];
41
+ let commit = '';
42
+ for (const line of blame.out.split('\n')) {
43
+ const head = /^([0-9a-f]{40}(?:[0-9a-f]{24})?) \d+ \d+/.exec(line);
44
+ if (head) {
45
+ commit = head[1];
46
+ continue;
47
+ }
48
+ if (line.startsWith('\t') && line.trim() !== '')
49
+ out.push({ entry: JSON.parse(line.slice(1)), commit });
50
+ }
51
+ return out;
52
+ }
30
53
  export function latestRecap(root, effort) {
31
54
  const recaps = readStream(root, effort).filter((e) => e.t === 'recap');
32
55
  return recaps.at(-1);
@@ -66,6 +66,8 @@ export async function run(ctx, argv) {
66
66
  continue; // already gone (re-slice, earlier abandon)
67
67
  if (artifact.meta.type !== 'plan')
68
68
  continue;
69
+ if (String(artifact.meta.status) === 'abandoned')
70
+ continue; // D189: an earlier abandon withdrew it
69
71
  // 'stamp-only': the spec loop below is this path's sole spec revert, pinned on
70
72
  // THIS effort's write sha. A paired revert here would re-derive the owner and
71
73
  // could land on a sibling effort's amendment — see Pairing in abandon.ts.
package/dist/verbs/log.js CHANGED
@@ -3,9 +3,9 @@ import { join } from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
4
  import { EXIT } from '../cli.js';
5
5
  import { askText } from '../ask.js';
6
- import { git, primaryRoot, tryGit } from '../gitio.js';
6
+ import { git, primaryRoot } from '../gitio.js';
7
7
  import { canonExit } from '../departure.js';
8
- import { journalRel, readStream, streamExists } from '../journal.js';
8
+ import { journalActs, readStream, streamExists } from '../journal.js';
9
9
  import { renderRefusal, v } from '../refusal.js';
10
10
  import { short } from '../sha.js';
11
11
  import { rows } from '../toon.js';
@@ -93,23 +93,12 @@ function commitIndex(root) {
93
93
  .forEach((sha, i) => map.set(sha, i));
94
94
  return map;
95
95
  }
96
- function lineOrigins(root, rel) {
97
- const blame = tryGit(root, 'blame', '--porcelain', '--', rel);
98
- if (!blame.ok)
99
- return [];
100
- const shas = [];
101
- for (const line of blame.out.split('\n')) {
102
- if (/^[0-9a-f]{40} \d+ \d+/.test(line))
103
- shas.push(line.slice(0, 40));
104
- }
105
- return shas;
106
- }
107
96
  export function mergedRows(root) {
108
97
  const order = commitIndex(root);
109
98
  const rowsOut = [];
110
99
  for (const stream of streamIds(root)) {
111
100
  const entries = readStream(root, stream);
112
- const origins = lineOrigins(root, journalRel(stream));
101
+ const origins = journalActs(root, stream).map((a) => a.commit);
113
102
  entries.forEach((entry, lineNo) => {
114
103
  const sha = origins[lineNo];
115
104
  rowsOut.push({ stream, entry, lineNo, idx: sha !== undefined ? (order.get(sha) ?? Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popovych.co/witness",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "Evidence-gated spec-driven development pipeline for AI coding agents: brainstorm to PR in six stages, where specs are state, plans are motion, and no transition passes without evidence. Drives from Claude Code or Pi.",
5
5
  "keywords": [
6
6
  "spec-driven-development",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witness",
3
3
  "description": "Spec-driven development with evidence gates: brainstorm → PR through six stages and five gates. Skills author, the pinned witness CLI writes state and witnesses evidence; gates stop where judgment is owed.",
4
- "version": "0.19.2",
4
+ "version": "0.19.3",
5
5
  "author": {
6
6
  "name": "Kostiantyn Popovych"
7
7
  },
@@ -12,7 +12,7 @@ Any other words in `$ARGUMENTS` are the human's ask: run `$WITNESS ask "<those w
12
12
  Resolve the CLI once:
13
13
 
14
14
  ```bash
15
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
15
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
16
16
  ```
17
17
 
18
18
  If the repo has no `witness.config.yaml`, run `$WITNESS init` first (one bootstrap commit), then proceed. If init or any verb refuses on a dirty tree, the human commits or cleans it — **never `git commit` yourself, anywhere, at any stage**: the CLI makes its own state commits, and the ship phase makes the sole code commit.
@@ -3,4 +3,4 @@
3
3
  # context). Silent everywhere that isn't a witness repo; failures swallowed:
4
4
  # session start must never hang or die on this.
5
5
  [ -f witness.config.yaml ] || exit 0
6
- ${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2} 2>/dev/null || true
6
+ ${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3} 2>/dev/null || true
@@ -15,7 +15,7 @@ witness: ends in a confirmed scope recap, writes no document. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -10,7 +10,7 @@ description: Slice a witness effort's confirmed recap into vertical spec slices,
10
10
  Resolve the CLI once per session:
11
11
 
12
12
  ```bash
13
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
14
14
  ```
15
15
 
16
16
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -16,7 +16,7 @@ No text copied verbatim; the design-critic lens is new material. -->
16
16
  Resolve the CLI once per session:
17
17
 
18
18
  ```bash
19
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
19
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
20
20
  ```
21
21
 
22
22
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ tests carry the parent spec's tag in their NAME. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ parent's criteria. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ model can. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.3}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.