@tech-leads-club/harness-toolkit 0.2.1 → 0.2.4
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/bin/tlc-cli.ts +20 -5
- package/dist/compact-before.mjs +86 -8
- package/dist/doctor.mjs +116 -10
- package/dist/init-project.mjs +13 -7
- package/dist/lessons-cli.mjs +86 -8
- package/dist/obs-cli.mjs +86 -8
- package/dist/prompt-submit.mjs +86 -8
- package/dist/response-after.mjs +86 -8
- package/dist/run.mjs +86 -8
- package/dist/session-end.mjs +86 -8
- package/dist/session-start.mjs +86 -8
- package/dist/shim.mjs +6976 -15
- package/dist/stop.mjs +86 -8
- package/dist/subagent-start.mjs +86 -8
- package/dist/subagent-stop.mjs +86 -8
- package/dist/support.mjs +86 -8
- package/dist/tlc-cli.mjs +106 -12
- package/dist/tool-after.mjs +86 -8
- package/dist/tool-before.mjs +86 -8
- package/dist/tool-failure.mjs +86 -8
- package/docs/log.md +5 -0
- package/package.json +3 -2
- package/src/core/core.facade.ts +11 -0
- package/src/core/index.ts +2 -0
- package/src/core/release/release.version.ts +147 -0
- package/src/core/shim/shim.precedence.ts +72 -0
- package/src/core/skill/skill.link.ts +81 -0
- package/src/entrypoints/shim.ts +41 -9
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/paths.ts +27 -0
- package/tools/doctor.ts +50 -3
- package/tools/init-project.ts +27 -7
- package/CHANGELOG.md +0 -95
- package/docs/decisions/ad-001.md +0 -32
- package/docs/decisions/ad-002.md +0 -51
- package/docs/decisions/ad-003.md +0 -30
- package/docs/decisions/ad-004.md +0 -37
- package/docs/decisions/ad-005.md +0 -36
- package/docs/decisions/ad-006.md +0 -49
- package/docs/decisions/ad-007.md +0 -36
- package/docs/decisions/ad-008.md +0 -54
- package/docs/decisions/ad-009.md +0 -61
- package/docs/decisions/ad-010.md +0 -45
- package/docs/decisions/ad-011.md +0 -59
- package/docs/decisions/ad-012.md +0 -71
- package/docs/decisions/ad-013.md +0 -87
- package/docs/decisions/ad-014.md +0 -56
- package/docs/decisions/ad-015.md +0 -33
- package/docs/decisions/ad-016.md +0 -98
- package/docs/decisions/ad-017.md +0 -65
- package/docs/decisions/ad-018.md +0 -77
- package/docs/decisions/ad-019.md +0 -75
- package/docs/decisions/ad-020.md +0 -88
- package/docs/decisions/ad-021.md +0 -57
- package/docs/decisions/ad-022.md +0 -120
- package/docs/decisions/ad-023.md +0 -87
- package/docs/decisions/ad-024.md +0 -85
- package/docs/decisions/ad-025.md +0 -92
- package/docs/decisions/ad-026.md +0 -131
- package/docs/decisions/ad-027.md +0 -90
- package/docs/decisions/ad-028.md +0 -90
- package/docs/decisions/ad-029.md +0 -73
- package/docs/decisions/ad-030.md +0 -97
- package/docs/decisions/ad-031.md +0 -94
- package/docs/decisions/ad-032.md +0 -83
- package/docs/decisions/ad-033.md +0 -89
- package/docs/decisions/ad-034.md +0 -86
- package/docs/decisions/ad-035.md +0 -86
- package/docs/decisions/ad-036.md +0 -68
- package/docs/decisions/ad-037.md +0 -47
- package/docs/decisions/ad-038.md +0 -52
- package/docs/decisions/ad-039.md +0 -69
- package/docs/decisions/ad-040.md +0 -89
- package/docs/decisions/ad-041.md +0 -98
- package/docs/decisions/ad-042.md +0 -82
- package/docs/decisions/ad-043.md +0 -79
- package/docs/decisions/ad-044.md +0 -61
- package/docs/decisions/ad-045.md +0 -94
- package/docs/decisions/ad-046.md +0 -111
- package/docs/decisions/ad-047.md +0 -96
- package/docs/decisions/ad-048.md +0 -85
- package/docs/decisions/ad-049.md +0 -66
- package/docs/decisions/ad-050.md +0 -94
- package/docs/decisions/ad-051.md +0 -69
- package/docs/decisions/ad-052.md +0 -69
- package/docs/decisions/ad-053.md +0 -78
- package/docs/decisions/ad-054.md +0 -98
- package/docs/decisions/ad-055.md +0 -74
- package/docs/decisions/ad-056.md +0 -85
- package/docs/decisions/ad-057.md +0 -68
- package/docs/decisions/ad-058.md +0 -97
- package/docs/decisions/ad-059.md +0 -82
- package/docs/decisions/ad-060.md +0 -75
- package/docs/decisions/ad-061.md +0 -68
- package/docs/decisions/ad-062.md +0 -72
- package/docs/decisions/ad-063.md +0 -84
- package/docs/decisions/ad-064.md +0 -79
- package/docs/decisions/ad-065.md +0 -81
- package/docs/decisions/ad-066.md +0 -111
- package/docs/decisions/ad-067.md +0 -64
- package/docs/decisions/ad-068.md +0 -79
- package/docs/decisions/ad-069.md +0 -74
- package/docs/decisions/ad-070.md +0 -86
- package/docs/decisions/ad-071.md +0 -93
- package/docs/decisions/ad-072.md +0 -82
- package/docs/decisions/ad-073.md +0 -102
- package/docs/decisions/ad-074.md +0 -91
- package/docs/decisions/ad-075.md +0 -79
- package/docs/decisions/ad-076.md +0 -102
- package/docs/decisions/ad-077.md +0 -94
- package/docs/decisions/ad-078.md +0 -84
- package/docs/decisions/ad-079.md +0 -73
- package/docs/decisions/ad-080.md +0 -86
- package/docs/decisions/ad-081.md +0 -70
- package/docs/decisions/ad-082.md +0 -79
- package/docs/decisions/ad-083.md +0 -88
- package/docs/decisions/index.md +0 -111
- package/tools/test-env.mjs +0 -28
- package/tools/test-env.names.d.mts +0 -1
- package/tools/test-env.names.mjs +0 -14
package/docs/decisions/ad-065.md
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-065 — The obs bus has a contract, and the gate checks both sides of it"
|
|
4
|
-
description: "Seven defects in one week shared one shape: a consumer reading where the producer does not write. Each became a decision record and the pattern never became an object, so it was rediscovered by hand seven times. A kind consumed and never emitted now fails the gate, and so does a consumer reading a plane its kinds never land on."
|
|
5
|
-
tags: [decision, observability, contracts, gates]
|
|
6
|
-
timestamp: "2026-08-12"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-065 — The obs bus has a contract, and the gate checks both sides of it
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-08-12
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
1. **A kind a consumer counts and no producer emits fails the gate.**
|
|
17
|
-
2. **A consumer declares its kinds and its planes**, and a kind landing on a plane the consumer does not read
|
|
18
|
-
fails. The plane comes from `resolveObsLevel` — the real function — so a change to the plane rules cannot
|
|
19
|
-
leave the check stale.
|
|
20
|
-
3. **A kind emitted and read by nothing is reported, not failed.** It costs a write per event and may be a rail
|
|
21
|
-
half-built; that is the operator's judgement.
|
|
22
|
-
4. **No registry file.** Emit sites are read from the source and consumers declare their own sets, so there is
|
|
23
|
-
nothing to keep in step.
|
|
24
|
-
|
|
25
|
-
## Why
|
|
26
|
-
|
|
27
|
-
**One pattern, seven instances, never materialised.** A consumer reading where the producer does not write:
|
|
28
|
-
`gate.outcome` and `policy.deny` consumed by the report and emitted by nothing
|
|
29
|
-
([/decisions/ad-027.md](/decisions/ad-027.md)); `format.enabled` in the catalog and read by no entrypoint
|
|
30
|
-
([/decisions/ad-033.md](/decisions/ad-033.md)); `progressiveHandoff` written and never read; the idle-turn
|
|
31
|
-
counter reading the signal plane while its kinds land on debug ([/decisions/ad-059.md](/decisions/ad-059.md));
|
|
32
|
-
`latestSessionId` ordering UUIDs by name; the comment gate diffing against `HEAD` after the turn had committed
|
|
33
|
-
([/decisions/ad-058.md](/decisions/ad-058.md)); the rollup summing a snapshot
|
|
34
|
-
([/decisions/ad-064.md](/decisions/ad-064.md)).
|
|
35
|
-
|
|
36
|
-
Every one was recorded. None of it stopped the next. Recording a fact is not the same as materialising the
|
|
37
|
-
pattern, and this repository already has two proofs that materialising works —
|
|
38
|
-
[/decisions/ad-041.md](/decisions/ad-041.md) made a union member read and never written a build failure, and
|
|
39
|
-
`check-screens.ts` did the same for a screen outside the standard.
|
|
40
|
-
|
|
41
|
-
**The practice transfers; the implementation does not exist.** Event-driven work treats a contract as a build
|
|
42
|
-
gate rather than as documentation — validate the producer's record, verify the consumer against it, fail the
|
|
43
|
-
pipeline on a mismatch. Orphan detection is the mirror: enumerate what is declared, check for references, attach
|
|
44
|
-
a cost to what nothing uses. Searching found no tooling that does either statically for an in-process bus, so
|
|
45
|
-
the pattern is borrowed and the code is ours.
|
|
46
|
-
|
|
47
|
-
**`ObsKind` was a schema, not a contract.** A closed union says which kinds exist. It does not say who emits
|
|
48
|
-
one, who reads it, or **which plane it lands on** — and the plane is exactly what the idle-turn rail got wrong.
|
|
49
|
-
`resolveObsLevel` decides it deterministically from the kind, which is what makes the check possible without a
|
|
50
|
-
runtime.
|
|
51
|
-
|
|
52
|
-
## Trade-offs
|
|
53
|
-
|
|
54
|
-
**It catches three of the seven.** The dead-kind class and the plane mismatch. It does nothing for the two
|
|
55
|
-
diff-base defects or the two aggregation defects, and saying otherwise would be the false coverage this exists
|
|
56
|
-
to remove. Those need a relation this cannot enumerate mechanically yet.
|
|
57
|
-
|
|
58
|
-
**The consumer list is hand-maintained.** One entry today. A consumer that never registers is invisible to the
|
|
59
|
-
check — the same shape as the defect it guards against, one level up. It is small enough to see in review and
|
|
60
|
-
large enough to be worth saying out loud.
|
|
61
|
-
|
|
62
|
-
**Emit sites are found by matching `kind:` literals.** A kind assembled at runtime would read as unemitted and
|
|
63
|
-
fail a consumer that reads it. Nothing does that today, and the failure is loud rather than silent.
|
|
64
|
-
|
|
65
|
-
**The orphan list found a defect on its first honest run.** Registering the session rollup and `why` took it
|
|
66
|
-
from thirteen to five, and two of the five were real: `policy.observe` and `cost.session_alert` were emitted and
|
|
67
|
-
read by nothing. Observation mode exists to produce a reading an operator acts on
|
|
68
|
-
([/decisions/ad-027.md](/decisions/ad-027.md)) and no command showed one — the rail recorded into a plane
|
|
69
|
-
nobody read. Both are now decisions in `why`, and three lifecycle kinds remain unread, which is fine and stated.
|
|
70
|
-
|
|
71
|
-
**An inline consumer cannot have a plane mismatch**, and the contract says so rather than passing it silently.
|
|
72
|
-
The rollup is fed at `recordObs` time, before plane routing; `why` and the activity counter read plane files.
|
|
73
|
-
Marking the difference is what separates "checked and fine" from "not checked".
|
|
74
|
-
|
|
75
|
-
## Not decided here
|
|
76
|
-
|
|
77
|
-
- **`generation.end`, `session.end` and `subagent.end`.** Emitted, read by nothing. They are lifecycle markers
|
|
78
|
-
and may stay that way; the report names them so it is a decision rather than an oversight.
|
|
79
|
-
- **Validating attributes per kind.** The payload half of a contract, and a much larger change. The rollup reads
|
|
80
|
-
`attrs.permission`, `attrs.rule` and others with no declaration that a producer sets them.
|
|
81
|
-
- **A runtime check.** This is a build gate; a hook has no budget for it.
|
package/docs/decisions/ad-066.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-066 — Uninstall reads the artefact, and the plan is the confirmation"
|
|
4
|
-
description: "The harness merges eleven hook groups into a settings.json that also holds the operator's permissions, sandbox and env, and offered no way out. The established answer is a receipt written at install time; that record would desync on the first npm upgrade or hand-edit, and every artefact this installer creates already names itself."
|
|
5
|
-
tags: [decision, install, safety, operator]
|
|
6
|
-
timestamp: "2026-08-12"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-066 — Uninstall reads the artefact, and the plan is the confirmation
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-08-12
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
1. **Ownership is read from the artefact, never from a record.** No manifest is written at install time and none
|
|
17
|
-
is required at uninstall time.
|
|
18
|
-
2. **A shared file is un-merged, never deleted.** `settings.json` keeps every key and every foreign hook group;
|
|
19
|
-
an event left with no groups loses its key rather than keeping an empty array.
|
|
20
|
-
3. **A symlinked runtime home is unlinked and never followed.**
|
|
21
|
-
4. **The split install already draws is the split uninstall honours.** `RUNTIME_PAYLOAD` goes, `OPERATOR_OWNED`
|
|
22
|
-
stays, and `--purge` is a separate word that `--yes` never implies.
|
|
23
|
-
5. **The plan is the confirmation and there is no prompt.** Without `--yes` the command prints the full plan and
|
|
24
|
-
changes nothing.
|
|
25
|
-
6. **The npm global package and per-project `.tlc/` are reported, not removed.**
|
|
26
|
-
|
|
27
|
-
## Why
|
|
28
|
-
|
|
29
|
-
**The exit was hand-editing a file most operators should not hand-edit.** `~/.claude/settings.json` carries
|
|
30
|
-
`permissions`, `sandbox`, `env` and `statusLine` next to our hook groups. Reverting the install meant finding
|
|
31
|
-
eleven groups in that file by eye. An install with no exit is one people try on one machine and never a second.
|
|
32
|
-
|
|
33
|
-
**The established pattern is a receipt, and it is the wrong one here.** CMake's `install_manifest.txt` is the
|
|
34
|
-
minimal form; package managers write the same record into a local store, and the schema people converge on
|
|
35
|
-
carries the path, a content hash, a `pre_existed` flag and the artefact type. The sharpest statement of the case
|
|
36
|
-
comes from a project that **rejected** an uninstall command outright: never infer ownership, because a bulk
|
|
37
|
-
command guessing which files are safe to remove will eventually guess wrong, and `--yes` turns that guess into a
|
|
38
|
-
recursive delete.
|
|
39
|
-
|
|
40
|
-
That objection is right about inference and does not describe this installer. Every artefact here names itself —
|
|
41
|
-
a hook group whose command *is* the launcher path, a symlink resolving inside the runtime home, a `hooks.json`
|
|
42
|
-
carrying the marker `isCursorWired` already checks. Matching an exact launcher path is not a heuristic. And a
|
|
43
|
-
receipt would be **strictly worse**, because the install routes are npm, git clone and a symlink to a working
|
|
44
|
-
checkout: three ways for a written record to desync from the disk on the first `npm i -g`, `git reset --hard` or
|
|
45
|
-
hand-edit. `isHarnessGroup` cannot desync — it reads what is there.
|
|
46
|
-
|
|
47
|
-
So the receipt's findings are adopted and its mechanism is not. `pre_existed` becomes "this group does not carry
|
|
48
|
-
the marker". "Skip a modified file" becomes "never follow a link into a checkout". "Remove a directory only when
|
|
49
|
-
empty" becomes the `OPERATOR_OWNED` split, which
|
|
50
|
-
[/decisions/ad-056.md](/decisions/ad-056.md) had already drawn for the installer.
|
|
51
|
-
|
|
52
|
-
**The symlinked home is the reason this is careful rather than a one-liner.** `~/.tlc/harness` is a link to the
|
|
53
|
-
contributor's working clone on every development machine, including the one this was written on. `rm -rf`
|
|
54
|
-
follows it and deletes the repository. `install.sh` refuses to touch a symlinked runtime for exactly this reason
|
|
55
|
-
([/decisions/ad-046.md](/decisions/ad-046.md)), and the uninstall inherits the refusal: `unlinkSync`, never
|
|
56
|
-
`rmSync` recursive.
|
|
57
|
-
|
|
58
|
-
**No prompt, because the operator reaching for this may have no terminal.** A confirmation prompt needs a TTY and
|
|
59
|
-
assumes an interactive session; the person undoing an install is as likely to be in CI, or in a shell the harness
|
|
60
|
-
itself has made unpleasant. Printing the plan and requiring `--yes` gives the same protection, works headless,
|
|
61
|
-
and leaves the plan in the log of whatever ran it.
|
|
62
|
-
|
|
63
|
-
**The npm package is reported rather than removed** because a global prefix owned by root needs `sudo`, and an
|
|
64
|
-
`npm uninstall` failing halfway through a teardown leaves a worse state than one that never started. Per-project
|
|
65
|
-
`.tlc/` is reported for a different reason: finding them means walking the operator's disk, which is a larger
|
|
66
|
-
risk than the residue.
|
|
67
|
-
|
|
68
|
-
## Trade-offs
|
|
69
|
-
|
|
70
|
-
**Running it found a defect the tests would not have.** On the machine it was written on,
|
|
71
|
-
`~/.claude/skills/harness-init` pointed at a `/tmp` install deleted weeks earlier. Under a target-based ownership
|
|
72
|
-
rule that dangling link reads as "not ours" and survives every uninstall forever. So the two link families now
|
|
73
|
-
answer to different rules: `tlc` on PATH is a name anybody may own and keeps the target test; `skills/harness-init`
|
|
74
|
-
is a path only this installer writes, so its **location** is the identification. Two rules is worse than one, and
|
|
75
|
-
leaving residue that carries our name is worse than two rules.
|
|
76
|
-
|
|
77
|
-
**A file that does not parse is left alone and reported.** The un-merge cannot preserve what it cannot read, and
|
|
78
|
-
the alternative — rewriting from a template — is precisely the data loss this exists to avoid. The operator gets
|
|
79
|
-
the path and the parse error.
|
|
80
|
-
|
|
81
|
-
**Two CI failures paid for the cross-platform rules, and both were the same mistake.** macOS: `realpathSync`
|
|
82
|
-
throws on a path whose tail is absent, so a dangling link fell back to its literal text and was compared against
|
|
83
|
-
a home that had been resolved — and the OS temp directory there sits under `/var`, a symlink to `/private/var`.
|
|
84
|
-
Windows: ownership was decided with `target.startsWith(`${root}/`)`, and that platform separates with `\`. In
|
|
85
|
-
both cases the harness's own artefact read as somebody else's and survived. One rule now: resolve both sides the
|
|
86
|
-
same way, and compare with `relative` rather than a string prefix. The path API is a parameter, so the win32
|
|
87
|
-
rules are asserted from any machine.
|
|
88
|
-
|
|
89
|
-
**And `install.ps1` does not write what `install.sh` writes.** It resolves the home from `USERPROFILE`, copies
|
|
90
|
-
`tlc.cmd` instead of linking it, and places one skill junction at `~/.tlc/skills/harness-init` rather than one
|
|
91
|
-
per provider directory. Reading the POSIX layout on Windows finds none of it and reports a clean machine, which
|
|
92
|
-
is the worst answer an uninstaller can give. A regular file is now identified by the launcher marker its one
|
|
93
|
-
command line carries, the same way every other artefact identifies itself.
|
|
94
|
-
|
|
95
|
-
**An artefact the operator renamed is invisible.** Ownership read from the artefact means an artefact that no
|
|
96
|
-
longer identifies itself is not ours to remove. This is the honest cost of decision 1, and it is quieter than a
|
|
97
|
-
receipt's failure mode, which is deleting something that has since become somebody else's.
|
|
98
|
-
|
|
99
|
-
**A partly-applied run leaves a mixed state, and that is why every step is idempotent.** A second run resumes
|
|
100
|
-
from wherever the first stopped and reports nothing to do once there is nothing left — including a link left
|
|
101
|
-
dangling by a payload removal that ran before it.
|
|
102
|
-
|
|
103
|
-
## Not decided here
|
|
104
|
-
|
|
105
|
-
- **Removing the harness from repositories.** `.tlc/` exists in every project it ran in. Searching for them is
|
|
106
|
-
the risk; the plan names the one-line removal instead.
|
|
107
|
-
- **Whether the Windows launcher copy is a bug.** `install.ps1` copies `bin\tlc.cmd` into the bin directory, and
|
|
108
|
-
that file resolves the launcher as `%~dp0tlc-exec.mjs` — relative to itself, where no launcher sits. The
|
|
109
|
-
uninstall removes the copy correctly; whether the install should have linked it is a separate question.
|
|
110
|
-
- **A `tlc harness reinstall`.** The one-liner already does it, and a verb that means "uninstall then install"
|
|
111
|
-
would need to decide what happens when the first half fails.
|
package/docs/decisions/ad-067.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-067 — A reserved file that cannot be retired is rendered, and the gate holds it there"
|
|
4
|
-
description: "docs/log.md is a reserved file of the OKF v0.1 bundle, so it cannot be deleted as a duplicate. Hand-maintained, it had drifted to 19 of 66 decision records. It is now rendered from the same files the changelog reads, with a gate step that fails on drift."
|
|
5
|
-
tags: [decision, docs, gates]
|
|
6
|
-
timestamp: "2026-08-12"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-067 — A reserved file that cannot be retired is rendered, and the gate holds it there
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-08-12
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
1. **`docs/log.md` is generated**, from the same `docs/decisions/` files the changelog reads.
|
|
17
|
-
2. **A fourteenth gate step**, `render-log.ts --check`, fails on drift.
|
|
18
|
-
3. **A record with no `timestamp` is dropped, never filed under a guessed date.**
|
|
19
|
-
4. **Days newest first, records within a day in id order** — because AD-064 was taken before AD-065, and
|
|
20
|
-
reading them the other way round inverts the reasoning.
|
|
21
|
-
|
|
22
|
-
## Why
|
|
23
|
-
|
|
24
|
-
**It had drifted to 19 of 66 and nobody noticed.** The log stopped at AD-017 with AD-051 and AD-052 grafted on
|
|
25
|
-
afterwards. Forty-seven records were missing, so a document describing itself as "the chronological record"
|
|
26
|
-
was 71% incomplete — worse than absent, because it reads as complete.
|
|
27
|
-
|
|
28
|
-
**Retiring it was the first answer and it was wrong.** The log duplicates `CHANGELOG.md`, which is already
|
|
29
|
-
rendered from the same records precisely so nobody maintains it by hand
|
|
30
|
-
([/decisions/ad-055.md](/decisions/ad-055.md)) — so deletion looked obvious. Reading
|
|
31
|
-
`tools/check-docs-bundle.ts` before acting found `RESERVED_FILES = new Set(["index.md", "log.md"])`:
|
|
32
|
-
`log.md` is a reserved file of the OKF v0.1 format this bundle adopted
|
|
33
|
-
([/decisions/ad-013.md](/decisions/ad-013.md)), with its own validation rule requiring ISO 8601 headings.
|
|
34
|
-
Deleting it would have left the bundle claiming a format it no longer follows.
|
|
35
|
-
|
|
36
|
-
**So the duplication is the constraint, not the defect.** The format requires two views of the same records:
|
|
37
|
-
`log.md` by date, `decisions/index.md` by number, and `CHANGELOG.md` by release. Three hand-maintained views
|
|
38
|
-
is three things to forget. Three rendered views is one source and no memory.
|
|
39
|
-
|
|
40
|
-
**The gate step is what makes it true tomorrow.** `render-capabilities --check` and `render-changelog --check`
|
|
41
|
-
already exist for exactly this, and both have caught staleness in this repository. A generated file with no
|
|
42
|
-
check is a hand-maintained file with extra steps.
|
|
43
|
-
|
|
44
|
-
## Trade-offs
|
|
45
|
-
|
|
46
|
-
**The log lost its prose.** The old entries carried narrative — what was measured, what was rejected — that a
|
|
47
|
-
title cannot. That narrative is in the decision record itself, which the entry links to, and the alternative
|
|
48
|
-
was 47 entries of narrative invented by whoever noticed the gap.
|
|
49
|
-
|
|
50
|
-
**`DecisionSummary` grew a field.** `timestamp` is now read alongside `title` and `migration`. It is optional,
|
|
51
|
-
so the changelog and the update digest are unaffected, and the reader was already parsing the frontmatter this
|
|
52
|
-
field lives in.
|
|
53
|
-
|
|
54
|
-
**Fourteen gate steps is a lot of steps.** Each one exists because something drifted while nobody was looking,
|
|
55
|
-
and the whole set runs in under thirty seconds. The number is a symptom of the checks being cheap, not of the
|
|
56
|
-
gate being heavy.
|
|
57
|
-
|
|
58
|
-
## Not decided here
|
|
59
|
-
|
|
60
|
-
- **Whether `CHANGELOG.md` should move into the bundle.** It sits at the repository root because npm and GitHub
|
|
61
|
-
both look for it there, and a bundle-relative link cannot reach it. The log now names it in prose.
|
|
62
|
-
- **Rendering `decisions/index.md` as well.** It is the third view of the same records and the only one still
|
|
63
|
-
hand-maintained. It was complete at 66 of 66 when this was written, so there is no evidence of drift to act
|
|
64
|
-
on yet.
|
package/docs/decisions/ad-068.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-068 — A directory decides what ships, and dist is derived from disk in both directions"
|
|
4
|
-
description: "A hand-maintained array named four development-only checks while ten qualified, so six that validate only this repository were bundled and copied into every install. The exclusion is now a directory, the payload copy honours it, and a bundle whose source is gone is pruned rather than shipped forever."
|
|
5
|
-
tags: [decision, packaging, boundaries, build]
|
|
6
|
-
timestamp: "2026-08-13"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-068 — A directory decides what ships, and dist is derived from disk in both directions
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
- **date**: 2026-08-13
|
|
13
|
-
|
|
14
|
-
## Decision
|
|
15
|
-
|
|
16
|
-
1. **`tools/dev/` is the declaration.** Every check that validates this repository's own architecture, docs or
|
|
17
|
-
conventions lives there, and the build reads one directory level, so nothing under it can become a bundle.
|
|
18
|
-
2. **The payload copy honours the same boundary.** `installRuntime` filters `tools/dev` and `tools/__test__`, and
|
|
19
|
-
the published `files` list excludes them.
|
|
20
|
-
3. **The clone route is deliberately different.** A checkout is the repository; a contributor needs these.
|
|
21
|
-
4. **A bundle whose source no longer exists is pruned by the build.**
|
|
22
|
-
|
|
23
|
-
## Why
|
|
24
|
-
|
|
25
|
-
**The array named four and ten qualified.** `bin/tlc-build` carried
|
|
26
|
-
`DEV_ONLY=(check-boundaries check-docs-bundle check-dist-fresh render-capabilities)`. Measured against the
|
|
27
|
-
repository as it stood: `check-wiring`, `check-screens`, `check-obs-contract`, `check-suppressions`,
|
|
28
|
-
`render-changelog`, `render-log` and `eval-skill-triggers` were all bundled into `dist/` and copied into every
|
|
29
|
-
install. Each validates something a user's project does not have — our module boundaries, our screen contract,
|
|
30
|
-
our observability kinds, our decision records.
|
|
31
|
-
|
|
32
|
-
**And the file says why that shape fails, two lines above it:**
|
|
33
|
-
|
|
34
|
-
> Derived from disk, never hardcoded: a fixed list silently stops building a new entrypoint, and the missing
|
|
35
|
-
> bundle only surfaces when a hook fires in production.
|
|
36
|
-
|
|
37
|
-
The principle was stated and then not applied to the exclusion. This is the same shape as every defect
|
|
38
|
-
[/decisions/ad-065.md](/decisions/ad-065.md) collected — a consumer that does not grow when the producer does —
|
|
39
|
-
with the list playing the consumer.
|
|
40
|
-
|
|
41
|
-
**Source ships too, and that was the larger half.** Excluding a bundle is not enough: `RUNTIME_PAYLOAD` copies
|
|
42
|
-
`tools` wholesale and the npm `files` list published it, so the TypeScript source arrived intact. With Bun
|
|
43
|
-
present the launcher resolves an entry straight from source, so `tlc-exec render-log` on a user's machine would
|
|
44
|
-
have rewritten files inside their runtime home.
|
|
45
|
-
|
|
46
|
-
**A directory cannot be forgotten.** A new checker either sits in `tools/dev/` and never ships, or sits in
|
|
47
|
-
`tools/` and does. There is no third state and nothing to remember, which is what separates this from the array
|
|
48
|
-
it replaces.
|
|
49
|
-
|
|
50
|
-
**dist only ever grew.** The build derived what to *compile* from disk and never what to *remove*, so a bundle
|
|
51
|
-
whose source moved stayed on disk, stayed committed, and stayed shipped — invisible to `check-dist-fresh`,
|
|
52
|
-
because an unchanged file produces no diff. Pruning made the move visible: seven bundles left, 31 to 24.
|
|
53
|
-
|
|
54
|
-
## Trade-offs
|
|
55
|
-
|
|
56
|
-
**Two directories now mean two things by their names.** `tools/` is the product, `tools/dev/` is the repository,
|
|
57
|
-
and the difference is only obvious once you know. The alternative was a marker inside each file, which is a
|
|
58
|
-
convention a reader can violate silently; a path is checked by the build every time.
|
|
59
|
-
|
|
60
|
-
**The tests stay flat in `tools/__test__/`.** They now reach across into `../dev/`, which is slightly worse to
|
|
61
|
-
read than co-location. The gate's glob is flat by design, and a second glob is a second thing to keep in step —
|
|
62
|
-
the exact failure being fixed.
|
|
63
|
-
|
|
64
|
-
**Pruning deletes files the operator did not ask to delete.** It is bounded to `dist/*.mjs`, which the build owns
|
|
65
|
-
entirely and rewrites on every run, and it names each file as it goes.
|
|
66
|
-
|
|
67
|
-
**A contributor's clone still has everything, so the boundary is untested there.** The sensor covers the two
|
|
68
|
-
product routes — the payload copy and the published file list — because those are the ones a user receives.
|
|
69
|
-
|
|
70
|
-
## Not decided here
|
|
71
|
-
|
|
72
|
-
- **Whether `eval-skill-triggers` should exist.** Nothing invokes it: not the gate, not CI, not a document. It
|
|
73
|
-
has a test, so it is exercised but not used. It moved to `tools/dev/` as the conservative placement; whether it
|
|
74
|
-
earns its place is a separate question.
|
|
75
|
-
- **Whether `docs/` should be filtered too.** The runtime payload copies our decision records so
|
|
76
|
-
`tlc harness update` can tell an operator what changed in the harness. That is the product's own release notes,
|
|
77
|
-
not a convention imposed on anyone, and it stays.
|
|
78
|
-
- **A check that fails when a shipped tool imports from `tools/dev`.** `tsc` catches the broken path today
|
|
79
|
-
because the directories differ; a rename could reintroduce the coupling without a type error.
|
package/docs/decisions/ad-069.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-069 — A decision record declares its shape, and cites by link so a move cannot break it"
|
|
4
|
-
description: "Sixty-seven records, all claiming to be current, with the body shape held only by habit and the citation form 96% consistent. The shape is now checked, the older twenty are a ratchet that may only fall, and a bare parenthesised id fails wherever a link is the convention."
|
|
5
|
-
tags: [decision, docs, gates]
|
|
6
|
-
timestamp: "2026-08-13"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-069 — A decision record declares its shape, and cites by link so a move cannot break it
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
|
|
13
|
-
## Decision
|
|
14
|
-
|
|
15
|
-
1. **Four headings are required**: `## Decision`, a heading beginning `## Why`, `## Trade-offs`, and
|
|
16
|
-
`## Not decided here`. Bespoke sections between them stay free-form.
|
|
17
|
-
2. **The older twenty are a ratchet**, not an exemption. The count off the current shape starts at 20 and the
|
|
18
|
-
gate fails if it rises **or** if it falls without the recorded budget following.
|
|
19
|
-
3. **A record is cited by link**, in documentation, in comments, and in test names.
|
|
20
|
-
4. **`status` is a closed set**, and it must agree with the folder the record sits in.
|
|
21
|
-
5. **The rule has no exemption list.** Its own examples are written so they do not match.
|
|
22
|
-
|
|
23
|
-
## Why
|
|
24
|
-
|
|
25
|
-
**The shape was a boundary, not a habit, and nothing knew it.** Measured: AD-021 through AD-067 carry all four
|
|
26
|
-
headings without exception; AD-001 through AD-020 use `## Applies to` and `## Consequences` instead. The format
|
|
27
|
-
changed once and held for forty-seven records. Held by nothing, though — the next record written under time
|
|
28
|
-
pressure would have dropped `## Trade-offs` and started a second boundary nobody noticed.
|
|
29
|
-
|
|
30
|
-
**`## Trade-offs` and `## Not decided here` are the two that get dropped, and the two that pay.** A record
|
|
31
|
-
without them reads as a decision with no cost and no open edges, which is the reading that makes somebody
|
|
32
|
-
re-litigate it six weeks later. This session alone, `## Not decided here` is where the Windows launcher question,
|
|
33
|
-
the per-record token accounting and the unused `eval-skill-triggers` went — each one a thing a reader would
|
|
34
|
-
otherwise assume was settled.
|
|
35
|
-
|
|
36
|
-
**A bare id is a citation only a reader who already knows can check.** The repository stood at 355 bundle-relative
|
|
37
|
-
links against 15 bare `(AD-NNN)` forms. A link survives a file move, `check-docs-bundle` resolves it, and a
|
|
38
|
-
reader can follow it; the bare form does none of that. Fifteen was small enough to fix in one pass and large
|
|
39
|
-
enough to keep growing — and the pass found twenty more inside the records themselves, which the first
|
|
40
|
-
measurement had missed by only looking at `docs/*.md`.
|
|
41
|
-
|
|
42
|
-
**The ratchet fails downward too.** A budget that only refuses to rise is one nobody ever turns: migrating a
|
|
43
|
-
record improves the number and nothing records that it happened. Failing when the count *falls* makes the
|
|
44
|
-
migration a two-line change — the record and the budget — and keeps the number honest.
|
|
45
|
-
|
|
46
|
-
**Backfilling the twenty would have meant inventing them.** Their trade-offs were either never written or are
|
|
47
|
-
now unknowable. A checker that allow-listed them by number would go stale the first time one was migrated, and
|
|
48
|
-
would hide the twenty-first. The count is the honest form.
|
|
49
|
-
|
|
50
|
-
## Trade-offs
|
|
51
|
-
|
|
52
|
-
**A ratchet at twenty is a number in a file that has to be maintained.** It is one integer, the gate names the
|
|
53
|
-
new value when it is wrong, and the alternative — twenty invented rationales — is worse in a way that cannot be
|
|
54
|
-
detected later.
|
|
55
|
-
|
|
56
|
-
**The Why rule matches by prefix, so `## Whyever` would pass.** Requiring the bare word would have forced
|
|
57
|
-
`## Why the runtime home had to change` down to `## Why`, which is worse prose in a record whose whole value is
|
|
58
|
-
prose. The looser rule buys better writing at the cost of a hole nobody has a reason to walk through.
|
|
59
|
-
|
|
60
|
-
**None of this reaches anyone using the harness.** It is a convention of this repository, checked by a tool in
|
|
61
|
-
`tools/dev/` that is never packaged ([/decisions/ad-068.md](/decisions/ad-068.md)). It makes the corpus cheaper
|
|
62
|
-
to read for whoever maintains the product, which reaches a user slowly and indirectly, or not at all.
|
|
63
|
-
|
|
64
|
-
**Two of the four `(AD-NNN)` forms this converted were inside test names**, where the linked form is long and
|
|
65
|
-
reads badly in a test report. The consistency was judged worth more than the line length, and the rule covers
|
|
66
|
-
tests precisely because that is where a convention erodes first.
|
|
67
|
-
|
|
68
|
-
## Not decided here
|
|
69
|
-
|
|
70
|
-
- **Whether the older twenty should be migrated at all.** The ratchet permits it and nothing requires it.
|
|
71
|
-
Whoever has the context for one may lower the number; nobody should do all twenty in one pass.
|
|
72
|
-
- **Whether `## Not decided here` should be required to be non-empty.** A heading with nothing under it
|
|
73
|
-
satisfies the checker today. Judging the content is a semantic question a gate cannot answer.
|
|
74
|
-
- **Applying the citation rule to `.specs/`.** That tree is gitignored and outside the documentation bundle.
|
package/docs/decisions/ad-070.md
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-070 — A comment has to read for somebody who was not in the session"
|
|
4
|
-
description: "The comment gate asked whether a comment declares a reason. It could not ask whether the reason means anything to a reader at HEAD, which is the comment a model actually writes — change narration, a decision number only the session saw, an argument aimed at a reviewer. A third mode asks that question, and the rule set was cut on measured false positives before it shipped."
|
|
5
|
-
tags: [decision, comment-policy, rails]
|
|
6
|
-
timestamp: "2026-08-13"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-070 — A comment has to read for somebody who was not in the session
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
|
|
13
|
-
## Decision
|
|
14
|
-
|
|
15
|
-
1. **A third mode, `resolvable`**, off by default and never implied: `declared` plus one further question — can
|
|
16
|
-
a reader at HEAD, with no transcript of the session, resolve every reference and check every claim?
|
|
17
|
-
2. **Five classes** are refused: change narration, dead citations, review vantage, reviewer-addressed
|
|
18
|
-
justification, and control-flow narration.
|
|
19
|
-
3. **Prose, never syntax.** Nothing here parses a language.
|
|
20
|
-
4. **One comment, one finding.** A block that trips three rules reports once, and a comment already refused for
|
|
21
|
-
having no declared reason is not refused twice.
|
|
22
|
-
5. **The refusal asks for a restatement, not a deletion.**
|
|
23
|
-
|
|
24
|
-
## Why
|
|
25
|
-
|
|
26
|
-
**The rail could not see the comment a model actually writes.** `declared` accepts anything carrying `why:`,
|
|
27
|
-
`hazard:` or `invariant:`, and `strict` accepts nothing. Neither can tell these apart:
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
// why: this used to read HEAD, which a committing turn moves past
|
|
31
|
-
// why: without the guard, a bare `*` reads as a block continuation
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Both declare a reason. The first is only checkable by somebody who saw the previous version; the second is
|
|
35
|
-
checkable by anybody, forever. The whole difference is vantage, and no marker carries it.
|
|
36
|
-
|
|
37
|
-
**The distinguishing question is a single one**, which is what makes it implementable: *could a reader at HEAD
|
|
38
|
-
resolve every reference and check every claim?* A counterfactual passes because the reader can construct it. A
|
|
39
|
-
past-tense claim about the code fails because the repository no longer holds the evidence.
|
|
40
|
-
|
|
41
|
-
**The restatement is the fix, not deletion.** Almost every unresolvable comment carries a true, durable fact
|
|
42
|
-
wrapped in the session's vantage. `this used to emit one finding per line, so one failing test arrived as three`
|
|
43
|
-
becomes `one finding per matched line makes a single failing test arrive as three` — same fact, no transcript
|
|
44
|
-
required. Telling the model to delete would throw away the reason along with the framing, so the message names
|
|
45
|
-
the restatement and only then permits deletion.
|
|
46
|
-
|
|
47
|
-
**Prose keeps it language-agnostic.** The syntax catalog exists because a delimiter per language is fragile and
|
|
48
|
-
does not scale ([/decisions/ad-058.md](/decisions/ad-058.md)). Resolvability is a property of the sentence, not
|
|
49
|
-
of the language, so a new language costs nothing.
|
|
50
|
-
|
|
51
|
-
## Trade-offs
|
|
52
|
-
|
|
53
|
-
**One phrase was measured out of the rule set before it shipped.** `no longer` was in the first draft. Run
|
|
54
|
-
against this repository's 1,077 comment blocks it matched four times, and every one described *runtime* state —
|
|
55
|
-
a lock owner that no longer exists, a lesson ref that no longer resolves, a path a future refactor would leave
|
|
56
|
-
behind. Zero described the repository's own history. Four wrong and none right is a phrase that teaches an
|
|
57
|
-
operator to switch the rail off ([/decisions/ad-034.md](/decisions/ad-034.md)), so it went.
|
|
58
|
-
|
|
59
|
-
**This repository would not pass its own new mode.** After that cut, 61 of 1,077 blocks still match — 5.7%, 55
|
|
60
|
-
of them `used to`. Sampling them, they are true positives: each carries a durable fact wrapped in history, and
|
|
61
|
-
each could be restated. The mode is off here, and saying so is more honest than weakening a correct rule until
|
|
62
|
-
the author's own code passes.
|
|
63
|
-
|
|
64
|
-
**Precision was chosen over recall everywhere.** Hedges, version stamps and untranslated fragments are all
|
|
65
|
-
recognisable leakage and none is matched, because none can be matched without also catching prose that is fine.
|
|
66
|
-
The rail catches less than a careful reader would.
|
|
67
|
-
|
|
68
|
-
**A regex is a blunt instrument for a semantic property.** `this is safe` is refused even when the sentence goes
|
|
69
|
-
on to state a real invariant. The refusal names the phrase, so the fix is one edit, and the alternative — a model
|
|
70
|
-
call per comment — is not something a stop hook has the latency budget for
|
|
71
|
-
([/decisions/ad-012.md](/decisions/ad-012.md)).
|
|
72
|
-
|
|
73
|
-
**A fourth mode is a fourth thing to explain.** The catalog's `asks` now offers three words rather than two, and
|
|
74
|
-
the operator prose the model reads is longer in this mode than in the others. The modes are ordered by strictness
|
|
75
|
-
and each is a superset of the last, which is the only arrangement that keeps three explainable.
|
|
76
|
-
|
|
77
|
-
## Not decided here
|
|
78
|
-
|
|
79
|
-
- **Whether `resolvable` should become the default.** It would change behaviour for every project that enabled
|
|
80
|
-
`comments`, and a capability changes nothing by default when it arrives.
|
|
81
|
-
- **Applying the same test to commit messages, plans or handoff prose.** The same leakage appears there and the
|
|
82
|
-
detector is not coupled to comments, but each surface needs its own calibration run before it can block.
|
|
83
|
-
- **Whether this repository should adopt the mode on itself.** It would mean restating 61 passages, and doing
|
|
84
|
-
that well needs the context of whoever wrote each one.
|
|
85
|
-
- **Hedges and planning residue.** `probably fine for now` is real leakage. Every pattern for it also matched
|
|
86
|
-
honest uncertainty, so it stays out until one exists that does not.
|
package/docs/decisions/ad-071.md
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: Decision
|
|
3
|
-
title: "AD-071 — The turn's added lines are checked against the code the project already has"
|
|
4
|
-
description: "Nothing watched for the agent writing something the codebase already contained. A line-run comparison over the turn's diff answers it without parsing a language, and three exclusions — comments, dependency declarations, pure data — were each added because a calibration run said so. It found a real duplication in this repository on its first honest pass."
|
|
5
|
-
tags: [decision, duplication, rails]
|
|
6
|
-
timestamp: "2026-08-13"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# AD-071 — The turn's added lines are checked against the code the project already has
|
|
10
|
-
|
|
11
|
-
- **status**: active
|
|
12
|
-
|
|
13
|
-
## Decision
|
|
14
|
-
|
|
15
|
-
1. **A rail, off by default**, that blocks the stop when this turn added a run of `minRun` lines or more that
|
|
16
|
-
already exists elsewhere in the project, naming both sites.
|
|
17
|
-
2. **Diff-scoped** against the sha the turn started from, like the comment gate. A run already duplicated before
|
|
18
|
-
the turn is not this turn's to answer for.
|
|
19
|
-
3. **Lines, never syntax.** Nothing parses a language.
|
|
20
|
-
4. **Three exclusions**, each added because a calibration run demanded it: comments, dependency declarations, and
|
|
21
|
-
runs that are not operational enough to be logic.
|
|
22
|
-
5. **Bounded and honest about it**: 2,000 files and 8 MB, and the scan reports when a bound was reached.
|
|
23
|
-
|
|
24
|
-
## Why
|
|
25
|
-
|
|
26
|
-
**Nothing was watching for the copy.** The harness had rails for narration, for evidence, for shell safety and
|
|
27
|
-
for the plan — and none for the failure an operator meets weeks later as two implementations of the same thing
|
|
28
|
-
drifting apart. The second copy is where the drift starts, and the second copy is exactly what a diff-scoped rail
|
|
29
|
-
can see.
|
|
30
|
-
|
|
31
|
-
**Three calibration runs shaped the rule, and each one changed it.**
|
|
32
|
-
|
|
33
|
-
| Run | Reported | What the top of the list was | What changed |
|
|
34
|
-
| --- | --- | --- | --- |
|
|
35
|
-
| first | 0 at every window | — | the index kept one site per run, so a run compared equal to itself |
|
|
36
|
-
| second | 137 at six lines | every import block in the repository | dependency declarations excluded |
|
|
37
|
-
| third | 116 | re-export lists, config literals, type bodies, fixtures | a run must be operational in the majority of its lines |
|
|
38
|
-
| fourth | 13 | two entrypoints building the same policy input | shipped |
|
|
39
|
-
|
|
40
|
-
The first run is the one worth naming: **zero findings looked like a clean repository and was a defect.** One
|
|
41
|
-
site per run meant the project index pointed at the very lines the turn had added, so every duplication compared
|
|
42
|
-
equal to itself and vanished. A rail that reports nothing is indistinguishable from a rail that has nothing to
|
|
43
|
-
report, which is why the calibration was run at four window lengths rather than one — four zeroes in a row is
|
|
44
|
-
not a property of a codebase.
|
|
45
|
-
|
|
46
|
-
**It found a real one immediately.** `subagent-start.ts` and `tool-before.ts` each assembled the same twelve-field
|
|
47
|
-
input for `evaluateSubagentSpawn`, identical apart from indentation. A new field in `policy.subagents` had to be
|
|
48
|
-
remembered in both — the shape [/decisions/ad-065.md](/decisions/ad-065.md) collected seven times, a consumer
|
|
49
|
-
that stops growing with its producer. It is now one function.
|
|
50
|
-
|
|
51
|
-
**Data is not logic, and repeated shape is what data is for.** A type's fields, a config literal and a re-export
|
|
52
|
-
list all repeat by design. Requiring a call, an assignment, a branch or a return in the majority of a run's lines
|
|
53
|
-
is what separates "these two do the same thing" from "these two describe the same thing", without asking what
|
|
54
|
-
language it is.
|
|
55
|
-
|
|
56
|
-
**Renaming makes it a different run, deliberately.** Normalisation collapses whitespace and a trailing comma —
|
|
57
|
-
the two things a paste changes without changing the code — and nothing else. Matching through renames would
|
|
58
|
-
report every similarly shaped function, which is the noise that ends a rail's life.
|
|
59
|
-
|
|
60
|
-
## Trade-offs
|
|
61
|
-
|
|
62
|
-
**It reads the repository on every stop.** `git ls-files` plus a read of each file is the honest way to answer
|
|
63
|
-
"does this exist anywhere", and hook latency is a product property ([/decisions/ad-012.md](/decisions/ad-012.md)).
|
|
64
|
-
The bounds are what keep a large repository from paying for it, and the scan says when it stopped early, because
|
|
65
|
-
a partial scan reporting nothing reads exactly like a clean one.
|
|
66
|
-
|
|
67
|
-
**Six lines is a judgement, not a discovery.** At four the matches were punctuation and import blocks; at ten
|
|
68
|
-
almost nothing survived. Six is where a match started being worth reading in *this* repository, and it is a
|
|
69
|
-
config field precisely because another project's answer will differ.
|
|
70
|
-
|
|
71
|
-
**Duplicated logic in tests is reported like any other.** Eleven of the thirteen remaining findings here are test
|
|
72
|
-
setup, and reasonable people call that acceptable. The rail does not know the difference, and inventing a
|
|
73
|
-
test-directory exemption would carve out the place duplication accumulates fastest.
|
|
74
|
-
|
|
75
|
-
**A copy that was renamed is invisible.** The rail catches the paste, not the paraphrase. Catching the paraphrase
|
|
76
|
-
needs a parser per language, which is the cost this design exists to avoid
|
|
77
|
-
([/decisions/ad-058.md](/decisions/ad-058.md)).
|
|
78
|
-
|
|
79
|
-
**Two exclusions are vocabulary, not grammar.** `import`, `require`, `use`, `#include` and their siblings open a
|
|
80
|
-
dependency declaration in most languages but not in all, and a language that spells it differently will have its
|
|
81
|
-
import blocks reported. The failure is loud and the fix is one word in a list.
|
|
82
|
-
|
|
83
|
-
## Not decided here
|
|
84
|
-
|
|
85
|
-
- **Whether to run the rail in observation mode first.** The harness supports recording a rail's findings without
|
|
86
|
-
blocking, and this one has a first-run false-positive history that argues for it. It ships off, so an operator
|
|
87
|
-
who wants the reading before the block can already have it.
|
|
88
|
-
- **Sharing the scan with the comment gate.** Both read the turn's added lines and both walk the project; today
|
|
89
|
-
each does its own. Merging them is a latency win and a coupling, and neither has been measured.
|
|
90
|
-
- **A cache keyed on the tree hash.** The index is rebuilt every stop. `git ls-files` plus a bounded read was
|
|
91
|
-
fast enough here to not need one, and a stale cache would report a duplication against code that has moved.
|
|
92
|
-
- **Whether the eleven test findings in this repository should be fixed.** They are real; whether shared setup is
|
|
93
|
-
worth the coupling is a judgement per test file.
|