@holmes-lab/holmes-kit 0.26.0 → 0.26.2
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/CHANGELOG.md +100 -0
- package/README.md +3 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/doctor.js +53 -10
- package/dist/holmes/cli/release-docs.d.ts +27 -0
- package/dist/holmes/cli/release-docs.js +45 -0
- package/dist/holmes/config/config.d.ts +11 -0
- package/dist/holmes/config/config.js +11 -1
- package/dist/holmes/cpg/cycle-report.d.ts +35 -0
- package/dist/holmes/cpg/cycle-report.js +74 -0
- package/dist/holmes/hooks/stop.js +18 -3
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +23 -7
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +239 -54
- package/dist/holmes/mcp/handlers.d.ts +12 -5
- package/dist/holmes/mcp/handlers.js +2 -0
- package/dist/holmes/mcp/tool-schemas.js +13 -0
- package/dist/holmes/project/root.js +11 -1
- package/dist/holmes/spec/entity-store.js +28 -6
- package/dist/holmes/spec/id-collision.d.ts +98 -0
- package/dist/holmes/spec/id-collision.js +152 -1
- package/dist/holmes/spec/remote-spec-refs.d.ts +25 -0
- package/dist/holmes/spec/remote-spec-refs.js +236 -0
- package/dist/holmes/spec/renumber.d.ts +12 -2
- package/dist/holmes/spec/renumber.js +25 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,106 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.26.2] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
The last stretch of the number-collision repair: the plan `doctor` has been printing since 0.26.1 can
|
|
11
|
+
now be executed — the plan a person saw, under one approval, before the merge.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`spec_reconcile` — the seen plan, one approval, before the merge** (A-SPEC-700.2). 0.26.1 detected a
|
|
15
|
+
spec number another machine took and printed the plan; executing it was still a person calling
|
|
16
|
+
`spec_renumber` once per family. `spec_reconcile({operation:'plan'})` is read-only: it chains the
|
|
17
|
+
existing collectors, collision detector, planner and `planRenumber` — no new judgement — and returns
|
|
18
|
+
the moves, how many documents / `depends_on` / anchors each rewrites, the re-seal order, the prose
|
|
19
|
+
candidates, and a `target` derived from the plan's content alone (no clock, no nonce: the same state
|
|
20
|
+
plans to the same target). `apply` takes that plan back, **rebuilds it inside the store hold**, and
|
|
21
|
+
refuses `plan-changed` if it is no longer the one that was seen — a fetch in between may have given
|
|
22
|
+
the destination away. It then demands one `config-write` approval bound to `target`, and runs every
|
|
23
|
+
move through the same path `spec_renumber` uses. Found while building it: `spec_renumber`'s own apply
|
|
24
|
+
asks for **no approval at all**, although it rewrites approved documents and source anchors; this
|
|
25
|
+
tool is the first path that honours REQ-700's "one approval" constraint, and the standalone tool is
|
|
26
|
+
unchanged (a separate decision). It never seals — `spec_approve` stays the only sealer, and the order
|
|
27
|
+
is returned. Prose is reported, never rewritten. No remote, or no git, is an answer (`moves: []` with
|
|
28
|
+
a note), not a refusal. Measured cost: the tool adds ≈414 schema tokens (41 tools ≈ 10,724).
|
|
29
|
+
An adversarial pass **run, not read**: forged `moves` under a true `target` are never read (what runs
|
|
30
|
+
is the recomputed plan); a grant is spent once and a refused apply spends nothing; a second move that
|
|
31
|
+
stops reports `moved` / `remaining` and planning again yields only the rest. It found one defect,
|
|
32
|
+
fixed here: with two colliding families that depend on each other, the re-seal order named a document
|
|
33
|
+
a sibling move had already renamed.
|
|
34
|
+
- `doctor`'s collision advice now ends by naming the tool: `… then re-seal with spec_approve, and merge
|
|
35
|
+
(spec_reconcile plan → apply runs every move under one approval)`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **A finished entity operation is no longer read as "interrupted" or "vanished"** (A-SPEC-701). The
|
|
39
|
+
pending-operation scan read each `journal.json` twice while retirement is a single rename, so a scan
|
|
40
|
+
racing a retirement could see the journal on the first read and not the second, and refuse
|
|
41
|
+
`recovery-required` for work that had just completed — measured as the Linux CI red on
|
|
42
|
+
`entity-store-boundaries` at a commit that changed no code. The scan reads once and skips an
|
|
43
|
+
operation whose directory has retired; the set of legal outcomes is not widened.
|
|
44
|
+
|
|
45
|
+
## [0.26.1] - 2026-09-20
|
|
46
|
+
|
|
47
|
+
Three judgements that were computed and reached nobody now reach a person, and a spec number another
|
|
48
|
+
machine took is seen before the merge instead of after the conflict. Work from two machines: the
|
|
49
|
+
Windows checkout found that the renumber tool had been dead there, and lost a slice to the collision
|
|
50
|
+
that tool exists to repair.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- **A spec id another workspace took is seen BEFORE the merge** (A-SPEC-254.2, A-SPEC-700, A-SPEC-700.1).
|
|
54
|
+
`doctor --target` gains `spec id collision with remote refs`. Measured 2026-09-20: two machines
|
|
55
|
+
allocated `REQ-694` thirteen minutes apart, neither could see the other, and it surfaced only as an
|
|
56
|
+
add/add conflict after the push was refused — costing a rebuilt slice and five out-of-band
|
|
57
|
+
approvals. The check reads each remote-tracking ref **as last fetched** (no network; `fetch` stays
|
|
58
|
+
yours) and judges only documents each side **added** since they parted: a spec one side merely
|
|
59
|
+
*edited* is ordinary divergence, not a collision, so this does not cry wolf before every pull. On a
|
|
60
|
+
collision the advice is the plan, not a shrug —
|
|
61
|
+
`Before merging — move 901 → 903: spec_renumber(oldBase=901, newBase=903); then re-seal …`.
|
|
62
|
+
The side that moves is the one **not published yet** (a public number may already be cited), and
|
|
63
|
+
the destination is free on **both** sides. "Published" is judged by equal content, never by path:
|
|
64
|
+
a spec's path is derived from its number, so a colliding number is on the remote by definition.
|
|
65
|
+
No git, or no remote, is a PASS that says so.
|
|
66
|
+
- **`architecture.cycleIgnore` in `ax.config.json`** (A-SPEC-694). The named exception the cycle
|
|
67
|
+
ratchet promised in 0.19 finally has a place to be written: path prefixes whose cycles are not the
|
|
68
|
+
project's to fix (vendored trees). A cycle is excepted only when **every** member lies under one;
|
|
69
|
+
one that straddles project code is still reported. `reference` does not pardon `reference-impl/`,
|
|
70
|
+
a prefix meaning "everything" is dropped, and a broken config excepts nothing. Excepted cycles stay
|
|
71
|
+
in the observation ledger, counted.
|
|
72
|
+
- **The release gate says how far the GitHub README has fallen behind** (A-SPEC-696). Syncing the
|
|
73
|
+
README that npm's `homepage` points at is a playbook step that runs *after* the publish, so no gate
|
|
74
|
+
could see it — and measured 2026-09-20 it had been skipped for seventeen README-changing releases.
|
|
75
|
+
The *next* release's gate now compares the remote README with each release tag's and reports
|
|
76
|
+
`matches v0.19.3 — 17 release(s) with README changes behind`, `diverged`, or `could not check (…)`.
|
|
77
|
+
Report only: nothing in that block can refuse a release.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- **The cycle ratchet had never spoken to anyone** (A-SPEC-694). Since the commit that introduced it,
|
|
81
|
+
the Stop hook computed the code cycles, wrote them to the observation ledger, and then called the
|
|
82
|
+
verdict **without them** — so the `ART-2` tracked line was dead code in the product, and 0.26.0's
|
|
83
|
+
widening of the ratchet to eight language families landed on the same dead path. Every unit test
|
|
84
|
+
passed because each handed the evidence in directly; the RED for this fix runs the *built hook as a
|
|
85
|
+
process*. The line is now emitted, bounded (five cycles, five members each, the rest **counted**):
|
|
86
|
+
measured here, one vendored cycle has 46 members. Still non-blocking — the hook supplies `track`.
|
|
87
|
+
- **`npm publish --dry-run` could not pass the release gate** (A-SPEC-695). npm exports
|
|
88
|
+
`npm_config_dry_run=true` to `prepublishOnly`; the gate handed it to jest; and the suite's own
|
|
89
|
+
`npx -p <tarball>` then installed nothing, so a green tree was refused as "not publishable". The
|
|
90
|
+
evidence environment now drops that one key — named, not a `npm_config_*` sweep, which would take
|
|
91
|
+
the registry and proxy settings with it.
|
|
92
|
+
- **`spec_renumber` was dead on Windows** (A-SPEC-699). The frontmatter fence and the `depends_on`
|
|
93
|
+
block were LF-only and `core.autocrlf=true` makes every spec CRLF, so the tool read **zero** specs
|
|
94
|
+
and reported "nothing to move" — indistinguishable from an empty store. It now reads them, and its
|
|
95
|
+
refusal carries how many it could not read.
|
|
96
|
+
- doctor's id-collision advice still read "manual until REQ-255" after REQ-255 had shipped; it names
|
|
97
|
+
`spec_renumber` now (A-SPEC-700.1).
|
|
98
|
+
|
|
99
|
+
### Changed
|
|
100
|
+
- Two Windows reds were the tests' own defects, not the product's (A-SPEC-697, A-SPEC-698): an
|
|
101
|
+
overlap fixture matched a `/`-joined suffix against a `path.join` path, so the refusal it exists to
|
|
102
|
+
assert had never been exercised on win32; and a supervisor teardown now retries the directory
|
|
103
|
+
removal Windows briefly holds after the owning process is gone.
|
|
104
|
+
- A-SPEC-692's `breaking_change` said "tests only" while its own Files to Touch listed three product
|
|
105
|
+
handlers; corrected to `gate-behavior` and re-sealed. A-SPEC-700's chain was corrected to describe
|
|
106
|
+
a planner over the existing detector rather than a second detector.
|
|
107
|
+
|
|
8
108
|
## [0.26.0] - 2026-09-20
|
|
9
109
|
|
|
10
110
|
Architecture you declare is now judged in every language the graph can follow, and the verdict
|
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 🤝 **The collision plan runs under one approval** *(new in 0.26.2)*: 0.26.1 saw a spec number another machine took and printed the plan; carrying it out was still one `spec_renumber` call per family, and that tool's apply asks for no approval although it rewrites approved documents and source anchors. `spec_reconcile` `plan` is read-only and returns the moves, what each rewrites, the re-seal order, the prose it will **not** touch, and a `target` derived from the plan's content alone. `apply` rebuilds the plan inside the store hold, refuses `plan-changed` if a fetch moved it since you looked, demands one `config-write` approval bound to that `target`, and runs every move through the path `spec_renumber` already uses. Forged `moves` are never read — what runs is the recomputed plan. It never seals: `spec_approve` stays the only sealer. No remote is an answer, not a refusal.
|
|
20
|
+
- 🔢 **A spec number another machine took is seen before the merge** *(new in 0.26.1)*: two checkouts allocated `REQ-694` thirteen minutes apart, neither could see the other, and it surfaced only as an add/add conflict after the push was refused — a rebuilt slice and five out-of-band approvals. `doctor --target` now reads each remote-tracking ref **as last fetched** (no network) and judges only the documents each side *added* since they parted, so a spec one side merely edited is divergence, not an alarm. On a collision the advice is the plan: `Before merging — move 901 → 903: spec_renumber(oldBase=901, newBase=903); then re-seal`. The side that moves is the one not published yet, the destination is free on both sides, and "published" means equal content — never path, because a spec's path is its number.
|
|
21
|
+
- 🔁 **The cycle ratchet finally speaks — and takes a named exception** *(new in 0.26.1)*: since the commit that introduced it, the Stop hook computed the code cycles and called the verdict without them, so the `ART-2` line had never been printed; every unit test passed because each handed the evidence in directly. It is emitted now, bounded (five cycles, five members each, the rest counted — one vendored cycle here has 46 members), and `ax.config.json` gains `architecture.cycleIgnore`: prefixes for trees that are not yours to fix. A cycle is excepted only when every member lies under one, `reference` does not pardon `reference-impl/`, a broken config excepts nothing, and excepted cycles stay counted in the ledger.
|
|
19
22
|
- 🧬 **Declare your architecture, in every language the graph can follow** *(new in 0.26.0)*: a C-SPEC's `## Forbidden Edges` takes a third rule kind beside `import` and `call` — `- inherits src/app -x-> ConcreteBase`, an exact match enforced at the pre-edit gate. Nothing is inferred: the rule reads the graph's `inherits` edges and never asks which class is "abstract", which is what makes it hold in Go, where interface satisfaction cannot be inferred at all. A project that already breaks the rule it wants adopts it with a **baseline that expires** — `- allow until 2026-09-27 : inherits src/legacy/a.ts -> LegacyBase`, exact on kind, file and target, never a prefix. Past its date the allowance still holds, because an expiry that blocked would turn the rule into a barrier to adoption; it is **reported** instead, carrying `(reconfirmed n since <date>)` so a twelfth renewal cannot hide.
|
|
20
23
|
- 🏛️ **The architecture verdict reaches the person closing the turn** *(new in 0.26.0)*: the scan-wide judgement existed and nothing in the product called it — measured, zero consumers outside the tests — so an expired baseline was neither blocked nor seen, and a time-boxed allowance could not be told from a permanent pardon. The Stop hook now prints `ARCH` lines on the non-blocking `tracked` channel: violations no baseline covers, expired baselines with their renewal count, and **which rule kind could not be judged in which extension** (`inherits rules NOT judged in: .js, .py, .tsx`) — asked per kind, over the files a rule of that kind actually judges, because a pooled answer called a language *judged* for a rule it could not see. No rule declared, nothing said and nothing paid: the judgement shares the one scan the cycle ratchet already makes (measured here: scan 6.6 s, judgement 0.14 s).
|
|
21
24
|
- ♻️ **The cycle ratchet judges every language it can resolve** *(new in 0.26.0)*: the Stop hook had re-implemented import resolution beside the graph builder's — relative specifiers only, four extensions. Measured on fixtures shaped the way each language requires: 16 import edges, 2 survived; 9 planted cycles, 1 found. It cost nothing in an all-TypeScript tree (919 edges either way), which is how it survived to 0.25, and everything in a Java, Go, Python or Rust consumer. There is now **one** resolver, consumed by both; a language whose specifiers do not resolve stays `unavailable` rather than reading as clean, and a wide Go package cycle is capped per cycle with the dropped members counted.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
29f20e72-mu9gw8dl
|
|
@@ -754,10 +754,12 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
754
754
|
// (id count) and stays untouched. OWN try/catch (round-1): sharing the block above meant an
|
|
755
755
|
// unrelated upstream throw silently erased these checks — a failure here must surface as WARN,
|
|
756
756
|
// never as a vanished line.
|
|
757
|
+
// @implements A-SPEC-254.2 — the local walk's entries, kept for the remote-ref check below. `undefined`
|
|
758
|
+
// means the walk itself failed, and then that check says so rather than judging an empty store.
|
|
759
|
+
let localIdEntries;
|
|
757
760
|
try {
|
|
758
|
-
const { detectIdCollisions } = require('../spec/id-collision');
|
|
761
|
+
const { detectIdCollisions, collisionKeyOf } = require('../spec/id-collision');
|
|
759
762
|
const { parseSpec } = require('../spec/spec-parser');
|
|
760
|
-
const crypto = require('node:crypto');
|
|
761
763
|
const specsRoot = path.join(target, '.ax', 'specs');
|
|
762
764
|
const entries = [];
|
|
763
765
|
let skipped = 0; // would-be specs present but not judged — a PASS must name its reduced scope (round-1)
|
|
@@ -769,13 +771,10 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
769
771
|
// (mirrors parseSpec), per-line trailing whitespace and blank lines dropped (formatting is not
|
|
770
772
|
// substance for an advisory WARN), plus the substantive frontmatter (type/title/dependsOn) via
|
|
771
773
|
// JSON array serialization — no field aliasing. The SEAL digest is untouched, as before.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return 'sha256:' + crypto.createHash('sha256')
|
|
777
|
-
.update(JSON.stringify([spec.type, spec.title, [...spec.dependsOn].sort(), body])).digest('hex');
|
|
778
|
-
};
|
|
774
|
+
// @implements A-SPEC-254.2 — that key now lives in `id-collision.ts` as `collisionKeyOf`, because the
|
|
775
|
+
// remote-ref collector below must produce the SAME key for the same document. It was moved, not
|
|
776
|
+
// rewritten: the round-1..3 reasoning above still describes it.
|
|
777
|
+
const collisionKey = collisionKeyOf;
|
|
779
778
|
// Symlinked directories are FOLLOWED (round-2): `Dirent.isDirectory()` is false for a dir
|
|
780
779
|
// symlink, so specs behind one silently left the judgment with skipped=0 — an unqualified PASS
|
|
781
780
|
// over an unexamined store. The realpath visited-set breaks symlink cycles. An UNREADABLE or
|
|
@@ -880,6 +879,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
880
879
|
}
|
|
881
880
|
};
|
|
882
881
|
collect(specsRoot);
|
|
882
|
+
localIdEntries = entries;
|
|
883
883
|
// No cross-reference to `spec readability` (round-3): that walker does not follow dir
|
|
884
884
|
// symlinks, so the pointer was false for symlink-reached files — doctor contradicting itself.
|
|
885
885
|
const scopeNote = skipped > 0 ? ` (${skipped} outside the verdict)` : '';
|
|
@@ -889,7 +889,10 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
889
889
|
add('spec id preemption collision', collisions.length === 0 ? 'PASS' : 'WARN', collisions.length === 0
|
|
890
890
|
? `No id is claimed by two different specs${scopeNote}`
|
|
891
891
|
: collisions.map((c) => `${c.id}: ${c.files.join(' ↔ ')} (${c.detail})`).join('; ') + scopeNote, collisions.length === 0 ? undefined
|
|
892
|
-
|
|
892
|
+
// @implements A-SPEC-700.1 — this line still told people to renumber BY HAND "until REQ-255" long after
|
|
893
|
+
// REQ-255 shipped, sending people to handwork the tool replaces. No plan is offered HERE:
|
|
894
|
+
// both copies sit in one store, so there is no published/unpublished side to choose by.
|
|
895
|
+
: 'Two specs preempted the same number — move one family with spec_renumber (dry-run first), then re-seal.');
|
|
893
896
|
add('spec id family coexistence', families.length === 0 ? 'PASS' : 'WARN', families.length === 0
|
|
894
897
|
? `No bare id coexists with its dot-suffix family${scopeNote}`
|
|
895
898
|
: families.map((f) => `${f.id}: ${f.files.join(', ')}`).join('; ') + scopeNote, families.length === 0 ? undefined
|
|
@@ -903,6 +906,46 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
903
906
|
add('spec id preemption collision', 'WARN', `could not run the check: ${msg}`);
|
|
904
907
|
add('spec id family coexistence', 'WARN', `could not run the check: ${msg}`);
|
|
905
908
|
}
|
|
909
|
+
// @implements A-SPEC-254.2 — the MERGE-TIME half of REQ-254. The walk above sees this checkout
|
|
910
|
+
// only, so a number another machine took stayed invisible until git refused the push (measured
|
|
911
|
+
// 2026-09-20: REQ-694 allocated twice, thirteen minutes apart, surfacing as an add/add conflict).
|
|
912
|
+
// Judged: documents each side ADDED since it parted from a remote-tracking ref — an EDITED spec
|
|
913
|
+
// is ordinary divergence, not a collision. Refs are read as they stand (no network; `fetch` is
|
|
914
|
+
// the caller's), the PASS names the refs it looked at so a stale ref cannot read as "no collision
|
|
915
|
+
// with the remote", and no git / no remote is a PASS with its reason (REQ-128). OWN try, for the
|
|
916
|
+
// round-1 reason: a failure here must not erase the two lines above.
|
|
917
|
+
try {
|
|
918
|
+
if (localIdEntries === undefined)
|
|
919
|
+
throw new Error('the local spec walk did not complete');
|
|
920
|
+
const { detectIdCollisions } = require('../spec/id-collision');
|
|
921
|
+
const { collectRemoteAddedSpecs } = require('../spec/remote-spec-refs');
|
|
922
|
+
const remote = collectRemoteAddedSpecs(target);
|
|
923
|
+
if (remote.unavailable !== undefined) {
|
|
924
|
+
add('spec id collision with remote refs', 'PASS', remote.unavailable);
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
const addedLocally = localIdEntries.filter((e) => !remote.baseFiles.has(e.file));
|
|
928
|
+
const found = detectIdCollisions([...addedLocally, ...remote.entries]).filter((i) => i.kind === 'id-collision');
|
|
929
|
+
// @implements A-SPEC-700.1 — the planner's first caller. WHICH number moves (the side not
|
|
930
|
+
// published yet) and WHERE (free on both sides) was decided by hand on 2026-09-20; it is a rule.
|
|
931
|
+
const { planIdReconcile, reconcileInputsFrom, reconcileAdvice } = require('../spec/id-collision');
|
|
932
|
+
const advice = found.length === 0 ? '' : reconcileAdvice(planIdReconcile(reconcileInputsFrom({ issues: found, local: localIdEntries, addedLocally, remote })));
|
|
933
|
+
// Names, bounded: a repository with dozens of stale branches must not turn one check into a
|
|
934
|
+
// paragraph. The count is always exact; the names are the first few, sorted.
|
|
935
|
+
const named = remote.refs.slice(0, 3).join(', ') + (remote.refs.length > 3 ? ` +${remote.refs.length - 3} more` : '');
|
|
936
|
+
const scope = ` (compared with ${remote.refs.length} remote-tracking ref(s) as last fetched: ${named || 'none comparable'}`
|
|
937
|
+
+ `${remote.skipped > 0 ? `; ${remote.skipped} unreadable` : ''})`;
|
|
938
|
+
add('spec id collision with remote refs', found.length === 0 ? 'PASS' : 'WARN', found.length === 0
|
|
939
|
+
? `No id added here was also added on a remote-tracking ref${scope}`
|
|
940
|
+
: found.map((c) => `${c.id}: ${c.files.join(' ↔ ')}`).join('; ') + scope, found.length === 0 ? undefined
|
|
941
|
+
: (advice !== '' ? `Before merging — ${advice}.`
|
|
942
|
+
: 'Another workspace took the same number. Move the side that is NOT published yet with spec_renumber BEFORE merging, then re-seal.'));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
catch (e) {
|
|
946
|
+
const msg = String(e instanceof Error ? e.message : e).split('\n')[0];
|
|
947
|
+
add('spec id collision with remote refs', 'WARN', `could not run the check: ${msg}`);
|
|
948
|
+
}
|
|
906
949
|
}
|
|
907
950
|
// @implements A-SPEC-176
|
|
908
951
|
// `list()` drops any file it cannot parse, which is correct for a gate but leaves the author
|
|
@@ -25,3 +25,30 @@ export interface ProfileDriftInput {
|
|
|
25
25
|
* is why the "current document" case is tested — a check that always speaks is not a check.
|
|
26
26
|
*/
|
|
27
27
|
export declare function profileDriftFindings(input: ProfileDriftInput): string[];
|
|
28
|
+
/**
|
|
29
|
+
* @implements A-SPEC-696
|
|
30
|
+
* Which release is the GitHub README a copy of — and how many README-changing releases has it missed?
|
|
31
|
+
*
|
|
32
|
+
* Step 6 of the publish playbook (sync the README npm's `homepage` points at) is prose, and it runs
|
|
33
|
+
* AFTER the publish, so that release's own gate can never check it. Measured 2026-09-20: the remote
|
|
34
|
+
* README was byte-identical to v0.19.0 and v0.19.3 while six release lines shipped, and nothing
|
|
35
|
+
* said so. The NEXT release's gate can see it: had the last sync happened, the remote would equal
|
|
36
|
+
* the latest tag's README.
|
|
37
|
+
*
|
|
38
|
+
* Pure — the caller does the network and the git. One sentence or `null`; `null` means there is
|
|
39
|
+
* nothing a person needs to do. A remote that could not be read is NEVER `null`: silence would read
|
|
40
|
+
* as "in step", which is exactly the silence six releases passed through.
|
|
41
|
+
*
|
|
42
|
+
* Byte comparison after folding line endings, and nothing cleverer. "Mostly the same" is not judged:
|
|
43
|
+
* a remote that matches no tag is reported as DIVERGED, not as behind, because the natural response
|
|
44
|
+
* to "behind" is an overwrite and someone may have edited the page on GitHub.
|
|
45
|
+
*/
|
|
46
|
+
export declare function externalReadmeLag(input: {
|
|
47
|
+
remote: string | null;
|
|
48
|
+
reason?: string;
|
|
49
|
+
/** Release tags with their README, OLDEST FIRST. */
|
|
50
|
+
tagged: ReadonlyArray<{
|
|
51
|
+
tag: string;
|
|
52
|
+
readme: string;
|
|
53
|
+
}>;
|
|
54
|
+
}): string | null;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.repoTargetFrom = repoTargetFrom;
|
|
13
13
|
exports.profileDriftFindings = profileDriftFindings;
|
|
14
|
+
exports.externalReadmeLag = externalReadmeLag;
|
|
14
15
|
/**
|
|
15
16
|
* The GitHub target this project publishes to, read from `package.json`.
|
|
16
17
|
*
|
|
@@ -66,3 +67,47 @@ function isShipped(version) {
|
|
|
66
67
|
return false;
|
|
67
68
|
return Number(m[1]) > 0 || Number(m[2]) > 0;
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* @implements A-SPEC-696
|
|
72
|
+
* Which release is the GitHub README a copy of — and how many README-changing releases has it missed?
|
|
73
|
+
*
|
|
74
|
+
* Step 6 of the publish playbook (sync the README npm's `homepage` points at) is prose, and it runs
|
|
75
|
+
* AFTER the publish, so that release's own gate can never check it. Measured 2026-09-20: the remote
|
|
76
|
+
* README was byte-identical to v0.19.0 and v0.19.3 while six release lines shipped, and nothing
|
|
77
|
+
* said so. The NEXT release's gate can see it: had the last sync happened, the remote would equal
|
|
78
|
+
* the latest tag's README.
|
|
79
|
+
*
|
|
80
|
+
* Pure — the caller does the network and the git. One sentence or `null`; `null` means there is
|
|
81
|
+
* nothing a person needs to do. A remote that could not be read is NEVER `null`: silence would read
|
|
82
|
+
* as "in step", which is exactly the silence six releases passed through.
|
|
83
|
+
*
|
|
84
|
+
* Byte comparison after folding line endings, and nothing cleverer. "Mostly the same" is not judged:
|
|
85
|
+
* a remote that matches no tag is reported as DIVERGED, not as behind, because the natural response
|
|
86
|
+
* to "behind" is an overwrite and someone may have edited the page on GitHub.
|
|
87
|
+
*/
|
|
88
|
+
function externalReadmeLag(input) {
|
|
89
|
+
if (input.remote === null)
|
|
90
|
+
return `could not check the GitHub README (${input.reason ?? 'unknown'})`;
|
|
91
|
+
const tagged = input.tagged ?? [];
|
|
92
|
+
if (tagged.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
// A CRLF working tree and the LF copy GitHub serves are one document (the lesson A-SPEC-692 left).
|
|
95
|
+
const fold = (text) => String(text).replace(/\r\n/g, '\n');
|
|
96
|
+
const remote = fold(input.remote);
|
|
97
|
+
// The NEWEST matching tag: v0.19.0 and v0.19.3 shared a README, and naming the older one would
|
|
98
|
+
// overstate the lag.
|
|
99
|
+
let at = -1;
|
|
100
|
+
for (let i = 0; i < tagged.length; i++)
|
|
101
|
+
if (fold(tagged[i].readme) === remote)
|
|
102
|
+
at = i;
|
|
103
|
+
if (at === -1)
|
|
104
|
+
return 'GitHub README matches no release tag — diverged, or edited on GitHub';
|
|
105
|
+
if (at === tagged.length - 1)
|
|
106
|
+
return null;
|
|
107
|
+
// Only releases that CHANGED the README count. A test-only patch release moves nothing a reader
|
|
108
|
+
// sees, and counting it would warn when there is nothing to sync.
|
|
109
|
+
const missed = tagged.slice(at + 1).filter((t) => fold(t.readme) !== remote).length;
|
|
110
|
+
if (missed === 0)
|
|
111
|
+
return null;
|
|
112
|
+
return `GitHub README matches ${tagged[at].tag} — ${missed} release(s) with README changes behind`;
|
|
113
|
+
}
|
|
@@ -25,6 +25,17 @@ export interface AxConfig {
|
|
|
25
25
|
storage: {
|
|
26
26
|
specStore: string;
|
|
27
27
|
};
|
|
28
|
+
architecture: {
|
|
29
|
+
/**
|
|
30
|
+
* @implements A-SPEC-694
|
|
31
|
+
* Areas whose code cycles are a NAMED EXCEPTION to the ART-2 ratchet — vendored trees the project
|
|
32
|
+
* cannot fix. A cycle is excepted only when EVERY member lies under one of these prefixes; one
|
|
33
|
+
* that straddles project code is still reported. Excepted cycles stay in the observation ledger,
|
|
34
|
+
* counted. It lives here, in a root file whose change passes a human, because an agent that
|
|
35
|
+
* could pardon its own violations would make the exception worthless.
|
|
36
|
+
*/
|
|
37
|
+
cycleIgnore: string[];
|
|
38
|
+
};
|
|
28
39
|
}
|
|
29
40
|
export declare const DEFAULT_CONFIG: AxConfig;
|
|
30
41
|
export declare function loadConfig(root: string): AxConfig;
|
|
@@ -50,6 +50,7 @@ exports.DEFAULT_CONFIG = {
|
|
|
50
50
|
guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true, preEditEvidence: 'warn', redFirstEvidence: 'track' },
|
|
51
51
|
highRiskDomains: ['@auth', '@payment'],
|
|
52
52
|
storage: { specStore: 'local-markdown' },
|
|
53
|
+
architecture: { cycleIgnore: [] },
|
|
53
54
|
};
|
|
54
55
|
function deepMerge(base, ov) {
|
|
55
56
|
if (ov == null || typeof ov !== 'object')
|
|
@@ -69,7 +70,16 @@ function loadConfig(root) {
|
|
|
69
70
|
// Fall back to the strict DEFAULT_CONFIG (enforcement:'block') so a broken file cannot silently
|
|
70
71
|
// weaken or disable governance — a bad config degrades to the safest posture, not to an exception.
|
|
71
72
|
try {
|
|
72
|
-
|
|
73
|
+
const merged = structuredClone(deepMerge(exports.DEFAULT_CONFIG, JSON.parse(fs.readFileSync(f, 'utf8'))));
|
|
74
|
+
// @implements A-SPEC-694 — `deepMerge` copies a non-object override as it arrived, so
|
|
75
|
+
// `"architecture": "x"` or `"cycleIgnore": "reference"` would reach the ratchet as written. An
|
|
76
|
+
// exception list must never fail TOWARD a wider exception: anything that is not an array of
|
|
77
|
+
// strings reads as no exception at all.
|
|
78
|
+
const declared = merged.architecture?.cycleIgnore;
|
|
79
|
+
merged.architecture = {
|
|
80
|
+
cycleIgnore: Array.isArray(declared) ? declared.filter((x) => typeof x === 'string') : [],
|
|
81
|
+
};
|
|
82
|
+
return merged;
|
|
73
83
|
}
|
|
74
84
|
catch {
|
|
75
85
|
return structuredClone(exports.DEFAULT_CONFIG);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two ends around a judgement that was already right.
|
|
3
|
+
*
|
|
4
|
+
* `cycleRatchetViolations(current, allowed)` has honoured a named exception since A-SPEC-574.4, and
|
|
5
|
+
* the Stop hook has supplied `allowed: []` since the same day — there was nowhere to name one. And
|
|
6
|
+
* what the judgement found had no bounded way to be said: `trackedLines` joins every detail of an
|
|
7
|
+
* article into ONE line, and measured 2026-09-20 this repository carries a 46-member cycle inside a
|
|
8
|
+
* vendored tree. Wiring the evidence without these two would have printed thousands of characters a
|
|
9
|
+
* turn about code nobody here can change — "a rule that blocks adoption is not a rule, it is a
|
|
10
|
+
* barrier" (REQ-574), in its observational form.
|
|
11
|
+
*
|
|
12
|
+
* Pure, and it judges nothing: one function fills the list the judgement reads, the other renders
|
|
13
|
+
* what the judgement returned. The caps bound RENDERING only — no verdict reads them (REQ-569 S5).
|
|
14
|
+
*/
|
|
15
|
+
import { type Cycle, type CycleViolation } from './cycle-detect';
|
|
16
|
+
/** How many cycles one ART-2 line names before it counts the rest. */
|
|
17
|
+
export declare const CYCLE_LINE_CAP = 5;
|
|
18
|
+
/** How many members of one cycle are named before the rest are counted. The ledger keeps them all. */
|
|
19
|
+
export declare const CYCLE_LINE_MEMBERS = 5;
|
|
20
|
+
/**
|
|
21
|
+
* The keys of the cycles that lie ENTIRELY inside ignored areas — what the hook supplies as `allowed`.
|
|
22
|
+
*
|
|
23
|
+
* Entirely, because a cycle that straddles project code and a vendored tree is the project's to
|
|
24
|
+
* fix: if one ignored member were enough, the declaration would become a way to hide the project's
|
|
25
|
+
* own cycles behind a neighbour.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ignoredCycleKeys(cycles: readonly Cycle[], prefixes: readonly string[]): string[];
|
|
28
|
+
/**
|
|
29
|
+
* The ratchet's violations as `tracked` details of bounded length.
|
|
30
|
+
*
|
|
31
|
+
* The wording is A-SPEC-574.4's (`code import cycle: a -> b -> a`), kept so nothing that reads it
|
|
32
|
+
* has to change. What is folded is COUNTED, per cycle and across cycles: a truncation that does not
|
|
33
|
+
* announce itself reads as the whole list. The full membership is in the observation ledger.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cycleStatusDetails(violations: readonly CycleViolation[], scopeSuffix: string): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CYCLE_LINE_MEMBERS = exports.CYCLE_LINE_CAP = void 0;
|
|
4
|
+
exports.ignoredCycleKeys = ignoredCycleKeys;
|
|
5
|
+
exports.cycleStatusDetails = cycleStatusDetails;
|
|
6
|
+
// @implements A-SPEC-694
|
|
7
|
+
/**
|
|
8
|
+
* The two ends around a judgement that was already right.
|
|
9
|
+
*
|
|
10
|
+
* `cycleRatchetViolations(current, allowed)` has honoured a named exception since A-SPEC-574.4, and
|
|
11
|
+
* the Stop hook has supplied `allowed: []` since the same day — there was nowhere to name one. And
|
|
12
|
+
* what the judgement found had no bounded way to be said: `trackedLines` joins every detail of an
|
|
13
|
+
* article into ONE line, and measured 2026-09-20 this repository carries a 46-member cycle inside a
|
|
14
|
+
* vendored tree. Wiring the evidence without these two would have printed thousands of characters a
|
|
15
|
+
* turn about code nobody here can change — "a rule that blocks adoption is not a rule, it is a
|
|
16
|
+
* barrier" (REQ-574), in its observational form.
|
|
17
|
+
*
|
|
18
|
+
* Pure, and it judges nothing: one function fills the list the judgement reads, the other renders
|
|
19
|
+
* what the judgement returned. The caps bound RENDERING only — no verdict reads them (REQ-569 S5).
|
|
20
|
+
*/
|
|
21
|
+
const cycle_detect_1 = require("./cycle-detect");
|
|
22
|
+
/** How many cycles one ART-2 line names before it counts the rest. */
|
|
23
|
+
exports.CYCLE_LINE_CAP = 5;
|
|
24
|
+
/** How many members of one cycle are named before the rest are counted. The ledger keeps them all. */
|
|
25
|
+
exports.CYCLE_LINE_MEMBERS = 5;
|
|
26
|
+
/**
|
|
27
|
+
* A prefix as the scanner spells paths: forward slashes, no leading `./`, no trailing `/`.
|
|
28
|
+
*
|
|
29
|
+
* A prefix that normalises to nothing (`''`, `.`, `/`, `./`) means "everything" and is DROPPED. One
|
|
30
|
+
* line that switches the whole ratchet off is not an exception, it is a switch, and a switch is a
|
|
31
|
+
* different decision from the one this field records.
|
|
32
|
+
*/
|
|
33
|
+
function normalisePrefix(raw) {
|
|
34
|
+
if (typeof raw !== 'string')
|
|
35
|
+
return null;
|
|
36
|
+
const p = raw.replace(/\\/g, '/').replace(/^(\.\/)+/, '').replace(/\/+$/, '').replace(/^\/+/, '');
|
|
37
|
+
return p === '' || p === '.' ? null : p;
|
|
38
|
+
}
|
|
39
|
+
/** At a path boundary only — `reference` must not pardon `reference-impl/` (shape is not location). */
|
|
40
|
+
const under = (file, prefix) => file === prefix || file.startsWith(`${prefix}/`);
|
|
41
|
+
/**
|
|
42
|
+
* The keys of the cycles that lie ENTIRELY inside ignored areas — what the hook supplies as `allowed`.
|
|
43
|
+
*
|
|
44
|
+
* Entirely, because a cycle that straddles project code and a vendored tree is the project's to
|
|
45
|
+
* fix: if one ignored member were enough, the declaration would become a way to hide the project's
|
|
46
|
+
* own cycles behind a neighbour.
|
|
47
|
+
*/
|
|
48
|
+
function ignoredCycleKeys(cycles, prefixes) {
|
|
49
|
+
const areas = (Array.isArray(prefixes) ? prefixes : []).map(normalisePrefix).filter((p) => p !== null);
|
|
50
|
+
if (areas.length === 0)
|
|
51
|
+
return [];
|
|
52
|
+
return cycles
|
|
53
|
+
.filter((c) => c.files.length > 0 && c.files.every((f) => areas.some((a) => under(f, a))))
|
|
54
|
+
.map((c) => (0, cycle_detect_1.cycleKey)(c.files));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The ratchet's violations as `tracked` details of bounded length.
|
|
58
|
+
*
|
|
59
|
+
* The wording is A-SPEC-574.4's (`code import cycle: a -> b -> a`), kept so nothing that reads it
|
|
60
|
+
* has to change. What is folded is COUNTED, per cycle and across cycles: a truncation that does not
|
|
61
|
+
* announce itself reads as the whole list. The full membership is in the observation ledger.
|
|
62
|
+
*/
|
|
63
|
+
function cycleStatusDetails(violations, scopeSuffix) {
|
|
64
|
+
const details = violations.slice(0, exports.CYCLE_LINE_CAP).map((v) => {
|
|
65
|
+
const shown = v.files.slice(0, exports.CYCLE_LINE_MEMBERS);
|
|
66
|
+
const folded = v.files.length - shown.length;
|
|
67
|
+
const more = folded > 0 ? ` -> (+${folded} more)` : '';
|
|
68
|
+
return `code import cycle: ${shown.join(' -> ')}${more} -> ${v.files[0]}${scopeSuffix}`;
|
|
69
|
+
});
|
|
70
|
+
const foldedCycles = violations.length - exports.CYCLE_LINE_CAP;
|
|
71
|
+
if (foldedCycles > 0)
|
|
72
|
+
details.push(`… and ${foldedCycles} more import cycle(s)${scopeSuffix}`);
|
|
73
|
+
return details;
|
|
74
|
+
}
|
|
@@ -92,6 +92,7 @@ const dist_freshness_1 = require("../project/dist-freshness");
|
|
|
92
92
|
const analysis_currency_1 = require("../project/analysis-currency");
|
|
93
93
|
const forbidden_edges_1 = require("../cpg/forbidden-edges");
|
|
94
94
|
const forbidden_edge_report_1 = require("../cpg/forbidden-edge-report");
|
|
95
|
+
const cycle_report_1 = require("../cpg/cycle-report");
|
|
95
96
|
// @implements A-SPEC-660 — the I/O half of the known-defect marker: walk the workspace's test files
|
|
96
97
|
// (the same directory rule and test predicate ART-4's anchor scan uses), parse each for markers,
|
|
97
98
|
// judge them against the injected clock. A walk that cannot START is NO SIGNAL (undefined) — never
|
|
@@ -668,8 +669,10 @@ function evaluateStop(specs, evidence) {
|
|
|
668
669
|
if (evidence?.cycles !== undefined && evidence.cycles.mode === 'track') {
|
|
669
670
|
const scope = evidence.cycles.scope.unavailable.length > 0
|
|
670
671
|
? ` (NOT judged: ${evidence.cycles.scope.unavailable.join(', ')})` : '';
|
|
671
|
-
|
|
672
|
-
|
|
672
|
+
// @implements A-SPEC-694 — rendered through `cycleStatusDetails`, which bounds the line: every
|
|
673
|
+
// detail of an article is joined into ONE line, and a 46-member cycle was measured in this tree.
|
|
674
|
+
const t = (0, cycle_report_1.cycleStatusDetails)((0, cycle_detect_1.cycleRatchetViolations)(evidence.cycles.current, evidence.cycles.allowed), scope)
|
|
675
|
+
.map((detail) => ({ article: 'ART-2', detail }));
|
|
673
676
|
if (t.length)
|
|
674
677
|
tracked = [...(tracked ?? []), ...t];
|
|
675
678
|
}
|
|
@@ -1299,6 +1302,14 @@ if (require.main === module) {
|
|
|
1299
1302
|
cycles = scanCodeCycles(root, sharedScan);
|
|
1300
1303
|
if (cycles === undefined)
|
|
1301
1304
|
throw new Error('cycle scan unavailable');
|
|
1305
|
+
// @implements A-SPEC-694 — the named exception A-SPEC-574.4 promised finally has a source.
|
|
1306
|
+
// A config that cannot be read excepts NOTHING: this list must never fail toward wider.
|
|
1307
|
+
try {
|
|
1308
|
+
cycles.allowed = (0, cycle_report_1.ignoredCycleKeys)(cycles.current, (0, config_1.loadConfig)(root).architecture.cycleIgnore);
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
cycles.allowed = [];
|
|
1312
|
+
}
|
|
1302
1313
|
// @implements A-SPEC-578.1 — record the observation the promotion criterion waits on.
|
|
1303
1314
|
// EVERY run, including a clean one: a false-positive rate is violations over chances, and a
|
|
1304
1315
|
// ledger that only speaks when something is wrong keeps the numerator and drops the
|
|
@@ -1426,7 +1437,11 @@ if (require.main === module) {
|
|
|
1426
1437
|
catch {
|
|
1427
1438
|
semantic = undefined;
|
|
1428
1439
|
}
|
|
1429
|
-
|
|
1440
|
+
// @implements A-SPEC-694 — `cycles` rides in the evidence below. It was computed and written
|
|
1441
|
+
// to the observation ledger from the founding commit on (071bbf0c) and never handed over, so
|
|
1442
|
+
// the ART-2 track branch and the constitution's strict branch were dead code in the product:
|
|
1443
|
+
// the ratchet had never spoken to anyone. The mode it carries is still the constant `track`.
|
|
1444
|
+
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}), ...(dist ? { dist } : {}), ...(analysis ? { analysis } : {}), ...(semantic ? { semantic } : {}), ...(forbiddenEdges ? { forbiddenEdges } : {}), ...(cycles ? { cycles } : {}) });
|
|
1430
1445
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
1431
1446
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
1432
1447
|
// @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SpecStore } from '../../spec/spec-store';
|
|
2
2
|
import { Approval } from '../../guardrail/risk-gate';
|
|
3
|
+
import { type RenumberPlan } from '../../spec/renumber';
|
|
3
4
|
export interface SpecLifecycleContext {
|
|
4
5
|
store: SpecStore;
|
|
5
6
|
resolveLedgerRoot(root?: string): {
|
|
@@ -64,26 +65,41 @@ export declare function createSpecLifecycleHandlers(context: SpecLifecycleContex
|
|
|
64
65
|
dependents: string[];
|
|
65
66
|
reason?: undefined;
|
|
66
67
|
}>;
|
|
68
|
+
/**
|
|
69
|
+
* @implements A-SPEC-700.2
|
|
70
|
+
* The plan a person SAW, executed under ONE approval, before the merge. `plan` reads; `apply`
|
|
71
|
+
* rebuilds the plan, refuses `plan-changed` if it is not the one that was seen, demands a
|
|
72
|
+
* `config-write` approval bound to the plan's `target`, then runs every move through the SAME
|
|
73
|
+
* path `spec_renumber` uses, inside one store hold. It never seals: `spec_approve` is the only
|
|
74
|
+
* sealer, so `approveOrder` is returned and the caller runs it.
|
|
75
|
+
*/
|
|
76
|
+
spec_reconcile(a: {
|
|
77
|
+
root?: string;
|
|
78
|
+
operation?: string;
|
|
79
|
+
plan?: unknown;
|
|
80
|
+
}): Promise<Record<string, unknown>>;
|
|
67
81
|
spec_renumber(a: {
|
|
68
82
|
root?: string;
|
|
69
83
|
oldBase: string;
|
|
70
84
|
newBase: string;
|
|
71
85
|
dryRun?: boolean;
|
|
72
86
|
}): Promise<{
|
|
87
|
+
unreadable?: string[] | undefined;
|
|
73
88
|
ok: boolean;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
reason: string;
|
|
90
|
+
dryRun?: undefined;
|
|
91
|
+
plan?: undefined;
|
|
77
92
|
movedSpecs?: undefined;
|
|
78
93
|
} | {
|
|
79
94
|
ok: boolean;
|
|
80
95
|
dryRun: boolean;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
reason?: undefined;
|
|
96
|
+
plan: RenumberPlan;
|
|
97
|
+
movedSpecs?: undefined;
|
|
84
98
|
} | {
|
|
85
99
|
ok: boolean;
|
|
86
|
-
|
|
100
|
+
dryRun: boolean;
|
|
101
|
+
movedSpecs: number;
|
|
102
|
+
plan: RenumberPlan;
|
|
87
103
|
}>;
|
|
88
104
|
/**
|
|
89
105
|
* @implements A-SPEC-538.1
|