@erclx/aitk 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +6 -4
- package/claude/skills/session-map/REQUIREMENT.md +57 -0
- package/claude/skills/session-map/SKILL.md +57 -0
- package/docs/agents/commands.md +2 -1
- package/docs/agents/index.md +1 -0
- package/docs/agents/superseded.md +57 -0
- package/docs/ai-workflow.md +1 -0
- package/package.json +1 -1
- package/src/binary.ts +16 -0
- package/src/commands/gov.ts +146 -0
- package/src/exempt-marker.ts +43 -0
- package/src/gov/superseded.ts +207 -0
- package/src/secrets/marker.ts +7 -21
- package/src/secrets/scan.ts +1 -12
- package/standards/session.md +4 -1
- package/standards/slug.md +8 -0
|
@@ -40,7 +40,7 @@ No surface carries cross-version sequencing, so report none. A row's `Waiting on
|
|
|
40
40
|
|
|
41
41
|
A compaction is a moment this skill cannot detect, so the human asks for each side of it and this skill reads the matching runbook when they do.
|
|
42
42
|
|
|
43
|
-
On a request to write the handoff or save the session, read `${CLAUDE_SKILL_DIR}/references/orchestrator-handoff.md` and follow it. It captures what the session learned
|
|
43
|
+
On a request to write the handoff or save the session, read `${CLAUDE_SKILL_DIR}/references/orchestrator-handoff.md` and follow it. It sends the generic half to `aitk:session-map`, which captures what the session learned and writes a session map per `${CLAUDE_SKILL_DIR}/../../standards/session.md`, then adds the decisions taken under delegated authority as this role's extension. That capture is the only one this session runs, since the refill sweep reports it as owed rather than paying it. Write nothing to the handoff that the board, a task file, or a groundwork folder already carries.
|
|
44
44
|
|
|
45
45
|
On a request to resume after a compaction, read `${CLAUDE_SKILL_DIR}/references/orchestrator-resume.md`, which reads that file back with the board and the groundwork behind the live work.
|
|
46
46
|
|
|
@@ -3,9 +3,11 @@ title: Orchestrator handoff runbook
|
|
|
3
3
|
description: The two sections an orchestrating session adds over the shared session map, how capture runs from a session that never commits, and the resume invocation the file carries out
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Write the pre-compaction handoff as orchestrator.
|
|
6
|
+
Write the pre-compaction handoff as orchestrator. Invoke `aitk:session-map` for the generic half, which is the filename, the three core sections, the write procedure, the drift step and the ref it reads, and the citation rule. Everything below is the extension this role adds over that core, and none of it belongs to a session holding no delegated authority.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Settle all three steps below before the door writes, so one write carries the core and the extension together. The door reports the map as written and knows nothing of this role, so its success line ends the generic half rather than this runbook, and a session that stops there ships a map missing both of the things this file exists to add.
|
|
9
|
+
|
|
10
|
+
1. Tell the door this session does not commit, which is the caveat its capture step takes and passes to `aitk:claude-memory-capture`.
|
|
9
11
|
2. Add `## Decisions taken under delegated authority` directly after `## State`, holding each decision and why it went that way, so nobody re-proposes it. It sits there rather than after the core because a decision is read against the state it was taken in.
|
|
10
12
|
3. Close the file with the block below, resolving `${CLAUDE_SKILL_DIR}/references/orchestrator-resume.md` and `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` to absolute paths as you write it and pasting each in place of `<RESUME_RUNBOOK>` and `<POLL_RUNBOOK>`:
|
|
11
13
|
|
|
@@ -25,6 +27,6 @@ The requirement is a resume request to the orchestrator skill with that skill lo
|
|
|
25
27
|
|
|
26
28
|
Step 1 exists because both other callers of capture are ship-chain skills and this session never ships. Without a call here, the session that receives every operator correction is the one session that records none. A compaction arriving with no warning takes the capture with it, and firing it once per batch of merges leaves the same window open across a long planning stretch, since a sweep runs only on a merge. The refill sweep reports the debt between handoffs so the operator knows one is owed.
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
Step 1 varies the door rather than replacing it, which is why the door is invoked first and this runbook states only what differs. The generic half moved out whole, the drift step and its ref recovery with it, so nothing here restates a step the door already carries and the two cannot disagree.
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
Capture is told this session does not commit, so it skips routing and writes memory files alone. A routed fact lands in a context entry, which is a tracked file, and the orchestrator's boundaries forbid writing one from this session. That split is correct rather than a limitation, since a domain fact belongs to the task that owns the surface and goes in that task's Findings, while what this session produces is feedback about how to work.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-map
|
|
3
|
+
description: Why the write procedure needs a route any session can take, why the door carries the drift step and its ref recovery, and why it states none of the shape the standard already fixes
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session map requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, the standard governing the pre-compaction handoff carries a complete numbered write procedure and nothing routes a session to it. The reading half already works, since the resume path opens the newest map and leads its report with it, so a file nobody is routed to write is read by a surface built to find it.
|
|
11
|
+
|
|
12
|
+
The only routed writer belongs to the orchestrator role. It disables model invocation, asserts that role on invocation, and adds a section for decisions taken under delegated authority, so a plain feature session that reaches it receives a section it cannot fill and a review poll it does not run.
|
|
13
|
+
|
|
14
|
+
The nearest description by wording writes a different artifact. Capture routes facts to a context entry and writes memory files, and a request naming the session and the moment matches it, so the map goes unwritten while a session believes it has handed off.
|
|
15
|
+
|
|
16
|
+
The drift step is the one most specific to session length and the easiest to skip. It reads the commit the session started from, nothing on the machine records that commit, and a route that names the step without naming how to recover the ref ships an instruction nobody can follow.
|
|
17
|
+
|
|
18
|
+
A step that reads a surface a project may not carry gets skipped on the reasoning that it has nothing to act on. A cold session run against a project with no `.claude/` tree skipped both the capture and the drift step and wrote its own rationale into the map, so the step the route exists to carry did not run on the first test from outside. The skip is worst where it looks most defensible, and a session that takes it quietly leaves a map indistinguishable from a conforming one.
|
|
19
|
+
|
|
20
|
+
A capture fired the same way for every caller breaks the one caller the split leaves. An orchestrating session may write no tracked file, so its capture has to skip routing, and a route offering no way to say so leaves that reader running capture twice or routing facts the role forbids it to fold.
|
|
21
|
+
|
|
22
|
+
A body that restates the sections, the frontmatter, or the numbered steps becomes a second source that drifts from the standard, which is the failure the split between the role writer and the core was made to remove rather than move.
|
|
23
|
+
|
|
24
|
+
## Must
|
|
25
|
+
|
|
26
|
+
- Write from any session whatever role it holds, without asserting one
|
|
27
|
+
- Cite the standard for the filename, the frontmatter, the sections, the numbered procedure, and the citation rule rather than restating any of them
|
|
28
|
+
- Run the drift step and state how to recover the ref it reads from how long the session has run
|
|
29
|
+
- Record what the drift verb names, and read a refusal as the boundary of what the verb can read
|
|
30
|
+
- Resolve the folder at the main worktree root, and write the file whole from the shell where the editing tools refuse that root
|
|
31
|
+
- Fall back to the branchless filename on an empty slug rather than stopping
|
|
32
|
+
- Leave model invocation enabled, so a session that has dropped its routing still reaches this by description
|
|
33
|
+
- Take a caller's statement that the session does not commit and pass it to capture, so no fact this route fires is routed into a tracked file
|
|
34
|
+
- Report the map alone, plus the line capture returns when a fact routed, since a fold left unreported is a fold nobody runs
|
|
35
|
+
|
|
36
|
+
## Must not
|
|
37
|
+
|
|
38
|
+
- Restate the standard's sections, frontmatter, or numbered steps
|
|
39
|
+
- Skip a step by predicting what its skill or verb would have answered, rather than running it and recording what came back
|
|
40
|
+
- Assert a role, or add a role's section to a map written by a session holding no such role
|
|
41
|
+
- Fold the capture step's output into this skill's report
|
|
42
|
+
- Take the worktree-local copy the editing tools offer in place of the main-root path
|
|
43
|
+
- Summarize the board, the task files, or the work that shipped, each of which is carried by an artifact a compaction leaves standing
|
|
44
|
+
|
|
45
|
+
## Guards
|
|
46
|
+
|
|
47
|
+
- Not a git repository: stop, since the filename comes from the branch
|
|
48
|
+
- Empty slug: fall back rather than stop, since the reasoning is worth more than the name
|
|
49
|
+
- The drift verb refusing: record the boundary under standing cautions and keep writing
|
|
50
|
+
|
|
51
|
+
## Out of scope
|
|
52
|
+
|
|
53
|
+
- Reading a handoff back at the start of the next session: `session-resume`
|
|
54
|
+
- Routing a session fact to the context entry that owns it, and writing what no entry owns to the memory folder: `claude-memory-capture`
|
|
55
|
+
- The sections a role adds over the core three, which belong to that role's own surface. `claude-orchestrate` owns the orchestrator's and cites this route for the generic half.
|
|
56
|
+
- Validating a written map against the standard, which no record kind covers, so a conforming shape rests on the standard being followed
|
|
57
|
+
- Firing the write without being asked, which is a question about what the harness supports and is measured on its own track
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-map
|
|
3
|
+
description: Writes the session map, the pre-compaction handoff at `.claude/tasks/session-<slug>.md`, from any session whatever role it holds, running the skill-drift step the write procedure opens with. Use when asked to "write the handoff", "write the session map", "save the session before it compacts", "we are about to compact", "hand off to the next session", or "leave a note for whoever picks this up". Do NOT use to route session facts to a context entry or the memory folder, which is `claude-memory-capture` and writes a different artifact, and do NOT use to read a handoff back, which is `session-resume`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session map
|
|
7
|
+
|
|
8
|
+
Write the pre-compaction handoff for the current session. `${CLAUDE_SKILL_DIR}/../../standards/session.md` fixes the filename, the frontmatter, the three core sections, the numbered write procedure, and the citation rule. Follow that document rather than this body, which states when the procedure runs and what it runs against and leaves the shape where it already lives.
|
|
9
|
+
|
|
10
|
+
Any session writes one. Do not assert a role on invocation, and do not add a role's sections to a map written by a session holding no such role.
|
|
11
|
+
|
|
12
|
+
Run every step rather than judging in advance that one has nothing to act on. The capture skill and the drift verb each answer for themselves, refusal included, and a refusal recorded is a result where a prediction of one is not. A project carrying none of the scaffolding either step reads is the case this fires on, and it is the case where skipping looks most defensible.
|
|
13
|
+
|
|
14
|
+
## Guards
|
|
15
|
+
|
|
16
|
+
- If `git rev-parse --git-dir` does not resolve, stop: `❌ Not a git repository. A session map takes its filename from the branch.`
|
|
17
|
+
- Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. An empty result falls back to `latest` rather than stopping, since a handoff is scratch and a stop loses the reasoning the file exists to save.
|
|
18
|
+
- Resolve the containing folder at the main worktree root with `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`. The guard above already stopped where that read has no repository to answer from, so it takes no fallback.
|
|
19
|
+
|
|
20
|
+
## Step 1: run the capture the procedure opens with
|
|
21
|
+
|
|
22
|
+
Item 1 of `## Writing one` is a capture. Invoke `aitk:claude-memory-capture` and let it return before writing, so the map cites what was written instead of restating the same lesson in prose.
|
|
23
|
+
|
|
24
|
+
Pass on the caveat a caller states about committing. A caller that does not commit says so, and capture then skips routing and writes memory files alone, since a routed fact lands in a context entry and that is a tracked file. A caller stating nothing leaves capture to decide for itself, which is the ordinary run.
|
|
25
|
+
|
|
26
|
+
Carry through the line capture returns when a fact routed, so the session knows a fold is still owed. Report nothing else about what it wrote. The map is this skill's output.
|
|
27
|
+
|
|
28
|
+
## Step 2: recover the commit the drift step reads
|
|
29
|
+
|
|
30
|
+
Item 2 runs `aitk claude skills drift <ref>` against the commit this session started from, and nothing on the machine records that commit. Estimate how long the session has run, round the duration up, and read the ref item 3 recovers:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git log -1 --format=%H --before='<duration> ago'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Round up rather than down. A ref older than the oldest load over-reports, and confirming a name it raises costs one read of that body, so the generous end is the safe one.
|
|
37
|
+
|
|
38
|
+
Record what the verb names under `## Standing cautions`. A refusal is the boundary of what the verb can read rather than a fault, since it answers where the working directory carries `claude/skills/` with history behind it and refuses in a project consuming the plugin from a cache. Record the boundary the verb reported and keep writing, rather than the one it looked likely to report.
|
|
39
|
+
|
|
40
|
+
## Step 3: write the map
|
|
41
|
+
|
|
42
|
+
Follow items 4 through 6. Write only what a compaction destroys and no other artifact already carries, and cite a commit, a task, or a file and line for every claim.
|
|
43
|
+
|
|
44
|
+
The file sits at the main worktree root, which the file-editing tools refuse from inside a linked worktree while offering a copy under that worktree instead. Take neither the refusal nor the copy. The map is written whole every time, so send it out as one plain shell command carrying a heredoc.
|
|
45
|
+
|
|
46
|
+
## Step 4: extend it only where a role is held
|
|
47
|
+
|
|
48
|
+
`## Extending it` governs a section added over the core three. Add one only when this session holds the role and the surface owning that role states the section, since a section the writer cannot fill teaches its reader to skip the file. A session holding no role writes the core three and stops.
|
|
49
|
+
|
|
50
|
+
## Output
|
|
51
|
+
|
|
52
|
+
```plaintext
|
|
53
|
+
✅ Session map written: .claude/tasks/session-<slug>.md
|
|
54
|
+
<what the drift verb named, or the boundary it refused at>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Emit the path from the project root. The `## Output` section of the project's instruction file sets the form it takes.
|
package/docs/agents/commands.md
CHANGED
|
@@ -48,6 +48,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
48
48
|
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
|
|
49
49
|
| `aitk claude skills reach` | Report the shipped bodies citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
50
50
|
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
51
|
+
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value (`--json`) |
|
|
51
52
|
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
52
53
|
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
53
54
|
| `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
@@ -65,7 +66,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
|
|
|
65
66
|
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
|
|
66
67
|
| `snippets` | `list`, `install`, `sync`, `create` |
|
|
67
68
|
| `standards` | `list`, `<name>` |
|
|
68
|
-
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order`
|
|
69
|
+
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order`, `superseded` |
|
|
69
70
|
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `skills reach`, `setup [dest]` |
|
|
70
71
|
| `wiki` | `init` |
|
|
71
72
|
| `design` | `render` |
|
package/docs/agents/index.md
CHANGED
|
@@ -29,6 +29,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
29
29
|
- [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
|
|
30
30
|
- [Citation reach](skills-reach.md): Reporting the shipped skill bodies that cite a path no target project receives, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
|
|
31
31
|
- [State-scoped risk](state-scoped-risk.md): Reading committed state rather than an arriving change, the shipped-tree corpus the secret scan reads, what it keys on and how a deliberate value is exempted, the advisory check and its network failure mode, and why one gates while the other reports
|
|
32
|
+
- [Superseded values](superseded.md): Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the exemption marker, the blind spot it cannot reach, and why it reports rather than gates
|
|
32
33
|
- [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
|
|
33
34
|
- [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, the refusal reasons, and why every write here runs through a verb
|
|
34
35
|
- [Test order](test-order.md): Reading where an implementation reached history ahead of its test, how a pair is decided, the three verdicts, the coverage the pairing cannot reach, and why the check reports rather than gates
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Superseded values
|
|
3
|
+
description: Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the exemption marker, the blind spot it cannot reach, and why it reports rather than gates
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Superseded values
|
|
7
|
+
|
|
8
|
+
`aitk gov superseded <superseded> <replacement>` reports where the tree still asserts a value the convention behind it no longer produces. It answers the case a diff cannot: a convention changes, and the declarations testing it keep asserting the old form with nothing pointing at them.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
aitk gov superseded feature-feat- feature-
|
|
12
|
+
aitk gov superseded feature-feat- feature- --json
|
|
13
|
+
aitk gov superseded old-name new-name --root ../my-app
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Option | Behavior |
|
|
17
|
+
| --------------- | ---------------------------------------------------------- |
|
|
18
|
+
| `--root <path>` | Tree to read, defaulting to the current directory |
|
|
19
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
20
|
+
|
|
21
|
+
Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
|
|
22
|
+
|
|
23
|
+
## Why the value is the key
|
|
24
|
+
|
|
25
|
+
A file-scoped map from the changed rule to what cites it reaches nothing here. A fixture asserting an old output names neither the rule nor the standard behind it, so the change that superseded the value touches no file the fixture can be found from. The value both sides carry is the only key there is.
|
|
26
|
+
|
|
27
|
+
The slug transform is what proved it. Dropping the type segment left four fixtures and three scenario scripts asserting the type-carrying spelling. One surfaced as a red sandbox arm, which named a failing scenario rather than a stale declaration, and the other six were found by deriving from the rule rather than by anything reporting them.
|
|
28
|
+
|
|
29
|
+
## What it reads
|
|
30
|
+
|
|
31
|
+
The whole corpus git lists: tracked files plus untracked ones git does not ignore. The untracked half keeps a file added on this branch in scope rather than one push later.
|
|
32
|
+
|
|
33
|
+
Matching is a literal substring, so a value carrying regular-expression characters means what it says. Every occurrence on a line reports under its own column, since a line carrying the value twice is two edits.
|
|
34
|
+
|
|
35
|
+
Binary files are skipped on a NUL byte rather than on an extension list, and so is any listed path that will not open. Both are counted. A run states the files it opened against the files git listed, because a count of what passed reads as a verdict on the repository unless it also says how much it left out.
|
|
36
|
+
|
|
37
|
+
Pass an empty replacement to retire a value outright, as in `aitk gov superseded feature-feat- ""`. Findings report the same way and none is annotated, since a line cannot carry a replacement that does not exist.
|
|
38
|
+
|
|
39
|
+
## The exemption marker
|
|
40
|
+
|
|
41
|
+
A declaration disagreeing with a convention for a stated reason carries `aitk-allow-superseded: <reason>` on its own line or the one directly above. The marker moves the line into the report's `Exempt` section, which is named rather than counted, so a reader weighing the report can reach the reason.
|
|
42
|
+
|
|
43
|
+
Only a marker naming a reason counts. A bare token is a line that meant to say something and did not, and honoring it would let a typo mute a finding. This is the `aitk-allow-secret` shape, and both read the same placement rule through one helper.
|
|
44
|
+
|
|
45
|
+
## The blind spot
|
|
46
|
+
|
|
47
|
+
The sweep sees the value and nothing else. A prose reference that went stale without carrying it is invisible here, which is not hypothetical: alongside the mechanical instances, one declaration had gone stale by citing the wrong standard for the transform, and it matches no string this sweep could have been given.
|
|
48
|
+
|
|
49
|
+
A value sweep closes most of this class and no part of that one. The help text and the frame say so on every run, so a clean report is not read as a clean tree.
|
|
50
|
+
|
|
51
|
+
## Exit codes
|
|
52
|
+
|
|
53
|
+
Exit codes are `0` when nothing asserts the superseded value, `1` for a refusal, and `2` for at least one finding. It refuses an empty superseded value, which would match every line rather than a convention, a superseded value equal to its replacement, which means no convention changed, and a tree git cannot list, since an empty list passes each of its zero files. An empty replacement is not among them, because retiring a value is an ordinary reason to run this.
|
|
54
|
+
|
|
55
|
+
Nothing wires this into `bun run check` or into a hook. A value sweep over-reports by construction, and gating a measure carrying a known false-positive class is what teaches contributors to route around the stage. `aitk gov test-order` is the sibling precedent.
|
|
56
|
+
|
|
57
|
+
An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the JSON record's `findings` array rather than the exit when a skill consumes this.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -250,6 +250,7 @@ This section is the corpus the coverage claim is measured against: every name `a
|
|
|
250
250
|
| ------------------------- | ------------------------------------------------------------------------------ |
|
|
251
251
|
| `aitk:claude-orchestrate` | To assert the control session that owns the queue and reviews each worker's PR |
|
|
252
252
|
| `aitk:session-resume` | At the start of a session, to pick up what a previous one left |
|
|
253
|
+
| `aitk:session-map` | At the close of a session, to write the handoff a compaction would destroy |
|
|
253
254
|
|
|
254
255
|
### Keep the project current with the toolkit
|
|
255
256
|
|
package/package.json
CHANGED
package/src/binary.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the bytes are something a line scanner should not read.
|
|
3
|
+
*
|
|
4
|
+
* A NUL byte rather than an extension list, since a shipped tree carries fonts
|
|
5
|
+
* and images under names no scanner has reason to enumerate, and a list would
|
|
6
|
+
* go stale the first time a format was added. Decoded text holds no NUL, so the
|
|
7
|
+
* test costs one scan and never rejects source.
|
|
8
|
+
*
|
|
9
|
+
* Neutral rather than owned by either sweep that calls it. The secret scan
|
|
10
|
+
* shaped it and the superseded-value sweep reads the same corpus, so leaving it
|
|
11
|
+
* in `src/secrets/` would put a `src/gov/` dependency on the secret scanner for
|
|
12
|
+
* a predicate about bytes.
|
|
13
|
+
*/
|
|
14
|
+
export function isBinary(text: string): boolean {
|
|
15
|
+
return text.includes('\0')
|
|
16
|
+
}
|
package/src/commands/gov.ts
CHANGED
|
@@ -14,6 +14,12 @@ import {
|
|
|
14
14
|
mergeExtraRules,
|
|
15
15
|
resolveRules,
|
|
16
16
|
} from '@/gov/stacks'
|
|
17
|
+
import {
|
|
18
|
+
readSuperseded,
|
|
19
|
+
SUPERSEDED_MARKER,
|
|
20
|
+
type SupersededHit,
|
|
21
|
+
type SupersededReport,
|
|
22
|
+
} from '@/gov/superseded'
|
|
17
23
|
import {
|
|
18
24
|
type PairRecord,
|
|
19
25
|
readTestOrder,
|
|
@@ -57,6 +63,11 @@ interface TestOrderOptions {
|
|
|
57
63
|
readonly json?: boolean
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
interface SupersededOptions {
|
|
67
|
+
readonly root?: string
|
|
68
|
+
readonly json?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
export function register(program: Command): void {
|
|
61
72
|
const gov = program
|
|
62
73
|
.command('gov')
|
|
@@ -180,6 +191,141 @@ export function register(program: Command): void {
|
|
|
180
191
|
.action((opts: TestOrderOptions) => {
|
|
181
192
|
process.exitCode = runTestOrder(opts)
|
|
182
193
|
})
|
|
194
|
+
|
|
195
|
+
gov
|
|
196
|
+
.command('superseded')
|
|
197
|
+
.description(
|
|
198
|
+
'Report where the tree still asserts a value a changed convention no longer produces',
|
|
199
|
+
)
|
|
200
|
+
.argument('<superseded>', 'The value the convention used to produce')
|
|
201
|
+
.argument('<replacement>', 'What it produces now')
|
|
202
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
203
|
+
.option('--root <path>', 'Tree to read, defaulting to the cwd')
|
|
204
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
205
|
+
.addHelpText(
|
|
206
|
+
'after',
|
|
207
|
+
[
|
|
208
|
+
'',
|
|
209
|
+
'Keyed on the value rather than on the file stating the rule. A fixture',
|
|
210
|
+
'asserting an old output names neither the rule nor the standard behind',
|
|
211
|
+
'it, so a file-scoped map from the changed rule reaches none of them and',
|
|
212
|
+
'the value both sides carry is the only key there is.',
|
|
213
|
+
'',
|
|
214
|
+
'It reports and never gates. A string appears for reasons unrelated to',
|
|
215
|
+
'the convention, so the output is a reading rather than a verdict, and a',
|
|
216
|
+
'declaration disagreeing for a stated reason carries a',
|
|
217
|
+
`\`${SUPERSEDED_MARKER}: <reason>\` marker on its own line or the one above.`,
|
|
218
|
+
'',
|
|
219
|
+
'Pass an empty replacement to retire a value outright. Findings report',
|
|
220
|
+
'the same way and none is annotated, since a line cannot carry a',
|
|
221
|
+
'replacement that does not exist.',
|
|
222
|
+
'',
|
|
223
|
+
'Blind spot:',
|
|
224
|
+
' a prose reference that went stale without carrying the value, such as',
|
|
225
|
+
' a declaration citing the wrong standard for the transform, matches',
|
|
226
|
+
' nothing here and is reached by reading rather than by this sweep',
|
|
227
|
+
'',
|
|
228
|
+
'Exit codes:',
|
|
229
|
+
' 0 nothing in the corpus asserts the superseded value',
|
|
230
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
231
|
+
' 2 at least one declaration still asserts it',
|
|
232
|
+
'',
|
|
233
|
+
'Examples:',
|
|
234
|
+
' aitk gov superseded feature-feat- feature-',
|
|
235
|
+
' aitk gov superseded feature-feat- feature- --json',
|
|
236
|
+
'',
|
|
237
|
+
].join('\n'),
|
|
238
|
+
)
|
|
239
|
+
.action(
|
|
240
|
+
async (
|
|
241
|
+
superseded: string,
|
|
242
|
+
replacement: string,
|
|
243
|
+
opts: SupersededOptions,
|
|
244
|
+
) => {
|
|
245
|
+
process.exitCode = await runSuperseded(superseded, replacement, opts)
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Reports and never gates, matching `test-order` above. The finding count moves
|
|
252
|
+
* the exit code with nothing wiring it into a push, since a value sweep
|
|
253
|
+
* over-reports by construction and gating a measure carrying a known
|
|
254
|
+
* false-positive class is what teaches contributors to route around a stage.
|
|
255
|
+
*/
|
|
256
|
+
async function runSuperseded(
|
|
257
|
+
superseded: string,
|
|
258
|
+
replacement: string,
|
|
259
|
+
opts: SupersededOptions,
|
|
260
|
+
): Promise<number> {
|
|
261
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
262
|
+
const report = await readSuperseded(root, { superseded, replacement })
|
|
263
|
+
const emitJson = opts.json ?? false
|
|
264
|
+
|
|
265
|
+
if (report.kind === 'unreadable') {
|
|
266
|
+
intro('aitk gov superseded')
|
|
267
|
+
logStep('Refused')
|
|
268
|
+
logError(report.reason)
|
|
269
|
+
outro()
|
|
270
|
+
|
|
271
|
+
if (emitJson) {
|
|
272
|
+
process.stdout.write(
|
|
273
|
+
`${JSON.stringify({ ok: false, reason: report.reason })}\n`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return 1
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
reportSuperseded(report, root)
|
|
281
|
+
|
|
282
|
+
if (emitJson) {
|
|
283
|
+
process.stdout.write(`${JSON.stringify({ ok: true, root, ...report })}\n`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return report.findings.length > 0 ? 2 : 0
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function describeHit(hit: SupersededHit): string {
|
|
290
|
+
const note = hit.carriesReplacement
|
|
291
|
+
? ' (the replacement is on this line)'
|
|
292
|
+
: ''
|
|
293
|
+
return `${hit.file}:${hit.line}:${hit.column}${note}: ${hit.preview}`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function reportSuperseded(
|
|
297
|
+
report: Extract<SupersededReport, { kind: 'measured' }>,
|
|
298
|
+
root: string,
|
|
299
|
+
): void {
|
|
300
|
+
intro('aitk gov superseded')
|
|
301
|
+
|
|
302
|
+
logStep('Sweep')
|
|
303
|
+
logInfo(`${report.superseded} → ${report.replacement} in ${root}`)
|
|
304
|
+
|
|
305
|
+
logStep(report.findings.length === 0 ? 'Clean' : 'Findings')
|
|
306
|
+
if (report.findings.length === 0) {
|
|
307
|
+
logInfo('nothing in the corpus asserts the superseded value')
|
|
308
|
+
} else {
|
|
309
|
+
for (const finding of report.findings) logWarn(describeHit(finding))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Named rather than counted. A muted line is a judgment someone recorded,
|
|
313
|
+
// and a reader weighing this report has to be able to reach the reason.
|
|
314
|
+
logStep('Exempt')
|
|
315
|
+
if (report.exempt.length === 0) {
|
|
316
|
+
logInfo('no line carries a marker')
|
|
317
|
+
} else {
|
|
318
|
+
for (const hit of report.exempt) logInfo(describeHit(hit))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// A count of what passed reads as a verdict on the repository unless the run
|
|
322
|
+
// also says how much of it the corpus left out.
|
|
323
|
+
logStep('Corpus')
|
|
324
|
+
logInfo(
|
|
325
|
+
`${report.files} file(s) opened of ${report.listed} listed, ${report.skipped} skipped as binary or unreadable`,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
outro()
|
|
183
329
|
}
|
|
184
330
|
|
|
185
331
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes every character a regular expression would read as syntax.
|
|
3
|
+
*
|
|
4
|
+
* The token became a parameter when this was extracted, and the two callers
|
|
5
|
+
* pass letters and hyphens alone. That is what makes escaping cheap here rather
|
|
6
|
+
* than a fix for a live defect: an unescaped token holding a dot matches the
|
|
7
|
+
* wrong lines and one holding a parenthesis throws, and neither failure is the
|
|
8
|
+
* caller's to anticipate.
|
|
9
|
+
*/
|
|
10
|
+
function escapeForPattern(token: string): string {
|
|
11
|
+
return token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether the line at `index` carries `token` with a reason after it, reading
|
|
16
|
+
* itself and the line above.
|
|
17
|
+
*
|
|
18
|
+
* Extracted from `src/secrets/marker.ts`, which shaped it, so the two sweeps
|
|
19
|
+
* that mute a line share one answer to where a marker may sit. Two lines rather
|
|
20
|
+
* than one, because a value is as often introduced by a preceding comment as
|
|
21
|
+
* annotated inline, and a format taking no trailing comment has nowhere else to
|
|
22
|
+
* put the marker. Nothing further up counts, so a marker cannot silence a block
|
|
23
|
+
* it does not sit on.
|
|
24
|
+
*
|
|
25
|
+
* Only a marker naming a reason counts. A bare token is read as a line that
|
|
26
|
+
* meant to say something and did not, and honoring it would let a typo mute a
|
|
27
|
+
* finding, where the reason is the whole value of an exemption a later reader
|
|
28
|
+
* has to weigh.
|
|
29
|
+
*/
|
|
30
|
+
export function isMarked(
|
|
31
|
+
lines: readonly string[],
|
|
32
|
+
index: number,
|
|
33
|
+
token: string,
|
|
34
|
+
): boolean {
|
|
35
|
+
const pattern = new RegExp(`${escapeForPattern(token)}:[ \\t]*\\S`)
|
|
36
|
+
const own = lines[index]
|
|
37
|
+
const above = index > 0 ? lines[index - 1] : undefined
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
(own !== undefined && pattern.test(own)) ||
|
|
41
|
+
(above !== undefined && pattern.test(above))
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { isBinary } from '@/binary'
|
|
4
|
+
import { isMarked } from '@/exempt-marker'
|
|
5
|
+
import { listRepositoryFiles } from '@/git-files'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The inline token exempting one line from this sweep, shaped on the
|
|
9
|
+
* `aitk-allow-secret` precedent and read by the same two-line rule.
|
|
10
|
+
*
|
|
11
|
+
* A declaration disagreeing with a convention for a stated reason is the case
|
|
12
|
+
* question 3 of the plan settled against gating on, and the marker is where
|
|
13
|
+
* that reason goes. A bare token names nothing a later reader can weigh, so it
|
|
14
|
+
* mutes nothing.
|
|
15
|
+
*/
|
|
16
|
+
export const SUPERSEDED_MARKER = 'aitk-allow-superseded'
|
|
17
|
+
|
|
18
|
+
export interface SupersededHit {
|
|
19
|
+
readonly file: string
|
|
20
|
+
/** One-based, matching the `file:line` form a reader clicks. */
|
|
21
|
+
readonly line: number
|
|
22
|
+
readonly column: number
|
|
23
|
+
/**
|
|
24
|
+
* Whether the replacement sits on the same line, outside the superseded
|
|
25
|
+
* occurrences themselves.
|
|
26
|
+
*
|
|
27
|
+
* Read outside them because the replacement is routinely a substring of the
|
|
28
|
+
* value it replaced, which is what a convention dropping a segment produces.
|
|
29
|
+
* A plain containment test answers true for every line under that shape and
|
|
30
|
+
* separates nothing.
|
|
31
|
+
*
|
|
32
|
+
* A reading aid rather than a filter. The line stating the change carries
|
|
33
|
+
* both values and so does a fixture halfway through a repair, and nothing
|
|
34
|
+
* here can tell those apart. Suppressing on it would hide the second case,
|
|
35
|
+
* which is the whole class this sweep exists to reach.
|
|
36
|
+
*/
|
|
37
|
+
readonly carriesReplacement: boolean
|
|
38
|
+
readonly preview: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SupersededOptions {
|
|
42
|
+
/** The value the convention used to produce, matched as a literal substring. */
|
|
43
|
+
readonly superseded: string
|
|
44
|
+
/** What it produces now, reported so a finding names what to write instead. */
|
|
45
|
+
readonly replacement: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SupersededReport =
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: 'measured'
|
|
51
|
+
readonly superseded: string
|
|
52
|
+
readonly replacement: string
|
|
53
|
+
/** Everything git listed, so the report states its own bound. */
|
|
54
|
+
readonly listed: number
|
|
55
|
+
/** Files opened, which is what the verdict actually covers. */
|
|
56
|
+
readonly files: number
|
|
57
|
+
/** Binary or unreadable, counted so a pass is not claimed over them. */
|
|
58
|
+
readonly skipped: number
|
|
59
|
+
readonly findings: readonly SupersededHit[]
|
|
60
|
+
readonly exempt: readonly SupersededHit[]
|
|
61
|
+
}
|
|
62
|
+
| { readonly kind: 'unreadable'; readonly reason: string }
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The longest preview a hit carries. A generated or minified line is one line
|
|
66
|
+
* to git and a screen of noise to a reader, and the report prints one hit per
|
|
67
|
+
* line found.
|
|
68
|
+
*/
|
|
69
|
+
const PREVIEW_LIMIT = 200
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether `replacement` appears on the line somewhere other than inside the
|
|
73
|
+
* superseded occurrences, which is what makes the flag mean anything when one
|
|
74
|
+
* value contains the other.
|
|
75
|
+
*
|
|
76
|
+
* An empty replacement carries nothing, so it answers false rather than the
|
|
77
|
+
* true every line returns from a containment test against the empty string.
|
|
78
|
+
* Retiring a value outright is what passes one, and reporting every finding as
|
|
79
|
+
* carrying its replacement there says the opposite of what happened.
|
|
80
|
+
*/
|
|
81
|
+
function carriesReplacement(line: string, options: SupersededOptions): boolean {
|
|
82
|
+
if (options.replacement === '') return false
|
|
83
|
+
return line.split(options.superseded).join('').includes(options.replacement)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every occurrence of `superseded` in one file's text, exemptions separated. */
|
|
87
|
+
export function sweepText(
|
|
88
|
+
file: string,
|
|
89
|
+
text: string,
|
|
90
|
+
options: SupersededOptions,
|
|
91
|
+
): { findings: SupersededHit[]; exempt: SupersededHit[] } {
|
|
92
|
+
const lines = text.split('\n')
|
|
93
|
+
const findings: SupersededHit[] = []
|
|
94
|
+
const exempt: SupersededHit[] = []
|
|
95
|
+
|
|
96
|
+
for (const [index, line] of lines.entries()) {
|
|
97
|
+
let column = line.indexOf(options.superseded)
|
|
98
|
+
if (column === -1) continue
|
|
99
|
+
|
|
100
|
+
const muted = isMarked(lines, index, SUPERSEDED_MARKER)
|
|
101
|
+
const bucket = muted ? exempt : findings
|
|
102
|
+
const trimmed = line.trim()
|
|
103
|
+
const preview =
|
|
104
|
+
trimmed.length > PREVIEW_LIMIT
|
|
105
|
+
? `${trimmed.slice(0, PREVIEW_LIMIT)}…`
|
|
106
|
+
: trimmed
|
|
107
|
+
const alsoReplacement = carriesReplacement(line, options)
|
|
108
|
+
|
|
109
|
+
while (column !== -1) {
|
|
110
|
+
bucket.push({
|
|
111
|
+
file,
|
|
112
|
+
line: index + 1,
|
|
113
|
+
column: column + 1,
|
|
114
|
+
carriesReplacement: alsoReplacement,
|
|
115
|
+
preview,
|
|
116
|
+
})
|
|
117
|
+
column = line.indexOf(options.superseded, column + 1)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { findings, exempt }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Where the tree still asserts a value the convention behind it no longer
|
|
126
|
+
* produces, keyed on the value rather than on the file stating the rule.
|
|
127
|
+
*
|
|
128
|
+
* The file-scoped map is what this exists against. A fixture asserting an old
|
|
129
|
+
* output names neither the rule nor the standard, so nothing reaches it from
|
|
130
|
+
* the change that superseded it, and the value it carries is the only key both
|
|
131
|
+
* sides share.
|
|
132
|
+
*
|
|
133
|
+
* This reports and never gates. A string appears for reasons unrelated to the
|
|
134
|
+
* convention, so the output is a reading a person settles, and the counts of
|
|
135
|
+
* what was listed, opened, and skipped are what keep it from reading as a
|
|
136
|
+
* verdict over the whole tree.
|
|
137
|
+
*
|
|
138
|
+
* What it cannot see is a prose reference that went stale without carrying the
|
|
139
|
+
* value, such as a declaration citing the wrong standard for the transform. A
|
|
140
|
+
* value sweep closes most of this class and no part of that one.
|
|
141
|
+
*/
|
|
142
|
+
export async function readSuperseded(
|
|
143
|
+
root: string,
|
|
144
|
+
options: SupersededOptions,
|
|
145
|
+
): Promise<SupersededReport> {
|
|
146
|
+
if (options.superseded === '') {
|
|
147
|
+
return {
|
|
148
|
+
kind: 'unreadable',
|
|
149
|
+
reason:
|
|
150
|
+
'The superseded value is empty, which matches every line in the tree rather than a convention.',
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.superseded === options.replacement) {
|
|
155
|
+
return {
|
|
156
|
+
kind: 'unreadable',
|
|
157
|
+
reason: `The superseded value and its replacement are both ${options.superseded}, so no convention changed and there is nothing to sweep for.`,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const listed = await listRepositoryFiles(root)
|
|
162
|
+
if (listed === undefined) {
|
|
163
|
+
return {
|
|
164
|
+
kind: 'unreadable',
|
|
165
|
+
reason: `Git listed no corpus under ${root}. An empty list passes each of its zero files, so a tree git cannot read refuses rather than reporting clean.`,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const findings: SupersededHit[] = []
|
|
170
|
+
const exempt: SupersededHit[] = []
|
|
171
|
+
let files = 0
|
|
172
|
+
let skipped = 0
|
|
173
|
+
|
|
174
|
+
for (const path of listed) {
|
|
175
|
+
let text: string
|
|
176
|
+
try {
|
|
177
|
+
text = await readFile(join(root, path), 'utf8')
|
|
178
|
+
} catch {
|
|
179
|
+
// A listed path that will not open is a symlink leaving the tree or a
|
|
180
|
+
// file removed since git answered. Counted rather than reported, so the
|
|
181
|
+
// run still states that it measured less than it listed.
|
|
182
|
+
skipped += 1
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (isBinary(text)) {
|
|
187
|
+
skipped += 1
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
files += 1
|
|
192
|
+
const swept = sweepText(path, text, options)
|
|
193
|
+
findings.push(...swept.findings)
|
|
194
|
+
exempt.push(...swept.exempt)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
kind: 'measured',
|
|
199
|
+
superseded: options.superseded,
|
|
200
|
+
replacement: options.replacement,
|
|
201
|
+
listed: listed.length,
|
|
202
|
+
files,
|
|
203
|
+
skipped,
|
|
204
|
+
findings,
|
|
205
|
+
exempt,
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/secrets/marker.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isMarked } from '@/exempt-marker'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The inline token exempting one line from the secret scan.
|
|
3
5
|
*
|
|
@@ -15,30 +17,14 @@
|
|
|
15
17
|
*/
|
|
16
18
|
export const SECRET_MARKER = 'aitk-allow-secret'
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* Only a marker naming a reason counts.
|
|
20
|
-
*
|
|
21
|
-
* A bare token is read as a line that meant to say something and did not,
|
|
22
|
-
* which is the rule `isStubSeed` already applies to a field set to anything
|
|
23
|
-
* but `true`. Honoring it would let a typo mute a finding, and the reason is
|
|
24
|
-
* the whole value of an exemption a later reader has to weigh.
|
|
25
|
-
*/
|
|
26
|
-
const MARKER_LINE = new RegExp(`${SECRET_MARKER}:[ \\t]*\\S`)
|
|
27
|
-
|
|
28
20
|
/**
|
|
29
21
|
* Whether the line at `index` is exempt, reading itself and the line above it.
|
|
30
22
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
23
|
+
* Where a marker may sit and what makes one count are `isMarked`, which the
|
|
24
|
+
* superseded-value sweep reads through the same call. Only the token differs
|
|
25
|
+
* between the two, so the placement rule has one answer rather than two that
|
|
26
|
+
* can drift.
|
|
35
27
|
*/
|
|
36
28
|
export function isExempt(lines: readonly string[], index: number): boolean {
|
|
37
|
-
|
|
38
|
-
const above = index > 0 ? lines[index - 1] : undefined
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
(own !== undefined && MARKER_LINE.test(own)) ||
|
|
42
|
-
(above !== undefined && MARKER_LINE.test(above))
|
|
43
|
-
)
|
|
29
|
+
return isMarked(lines, index, SECRET_MARKER)
|
|
44
30
|
}
|
package/src/secrets/scan.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { isBinary } from '@/binary'
|
|
3
4
|
import { listRepositoryFiles } from '@/git-files'
|
|
4
5
|
import { isExempt } from '@/secrets/marker'
|
|
5
6
|
import { matchLine } from '@/secrets/patterns'
|
|
@@ -48,18 +49,6 @@ export type SecretScan =
|
|
|
48
49
|
}
|
|
49
50
|
| { readonly kind: 'refused'; readonly reason: ScanRefusal }
|
|
50
51
|
|
|
51
|
-
/**
|
|
52
|
-
* Whether the bytes are something a line scanner should not read.
|
|
53
|
-
*
|
|
54
|
-
* A NUL byte rather than an extension list, since the shipped tree carries
|
|
55
|
-
* fonts and images under names this check has no reason to enumerate, and a
|
|
56
|
-
* list would go stale the first time a format was added. Decoded text holds no
|
|
57
|
-
* NUL, so the test costs one scan and never rejects source.
|
|
58
|
-
*/
|
|
59
|
-
export function isBinary(text: string): boolean {
|
|
60
|
-
return text.includes('\0')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
52
|
/** Every finding in one file's text, with the marker already applied. */
|
|
64
53
|
export function scanText(file: string, text: string): SecretFinding[] {
|
|
65
54
|
const lines = text.split('\n')
|
package/standards/session.md
CHANGED
|
@@ -56,9 +56,11 @@ Add a section only for content that fits none of the three and would otherwise b
|
|
|
56
56
|
|
|
57
57
|
## Writing one
|
|
58
58
|
|
|
59
|
+
The `session-map` skill is the invocable route onto this procedure and holds no role, so a session reaching for a handoff without knowing this document exists still lands here. A session that arrives at this document directly follows the steps below unchanged.
|
|
60
|
+
|
|
59
61
|
1. Capture what the session learned first, so the map cites what was written instead of restating the same lesson in prose.
|
|
60
62
|
2. Run `aitk claude skills drift <the commit this session started from>` and record what it names under `## Standing cautions`. A skill body enters a session once and re-invoking the skill does not re-read the file, so the drift is worst at exactly this moment and a name here is a body the session has been following out of date. Recording a name is not acting on it, and the verb misses a body this session edited and has not committed. This step belongs to session length rather than to any role, so every writer runs it, and a refusal names the boundary of what the verb can read rather than a fault.
|
|
61
|
-
3. Recover that commit from how long the session has been running with `git log -1 --format=%H --before='<duration> ago'`, rounding the duration up rather than down. Nothing on the machine records it. A ref older than the oldest load over-reports, and confirming a name costs one read of the body, so the generous end is the safe one and a guess at the exact commit is not worth making.
|
|
63
|
+
3. Recover that commit from how long the session has been running with `git log -1 --format=%H --before='<duration> ago'`, rounding the duration up rather than down. Nothing on the machine records it. A ref older than the oldest load over-reports, and confirming a name costs one read of the body, so the generous end is the safe one and a guess at the exact commit is not worth making. An empty result means no commit predates the window, so take the oldest commit the history holds and say which of the two the ref came from.
|
|
62
64
|
4. Write only what a compaction destroys and no other artifact already carries. The board holds the ordering and what each task waits on, a task file holds its own findings, and a measurement folder holds its track.
|
|
63
65
|
5. Cite a commit, a task, or a file and line for every claim, so the next session can tell a read from a recall.
|
|
64
66
|
6. Fill a section from reasoning the session actually holds. A session with no cross-feature picture that fills `## State` from what is already in git has written a summary of the tree, which the reader can produce faster than they can read it.
|
|
@@ -76,6 +78,7 @@ Do not restate the board, and do not summarize the work that shipped, since vers
|
|
|
76
78
|
- Carry every core section whatever else a role adds, and state in the surface that adds a section why the core cannot carry it
|
|
77
79
|
- Let the adding surface fix where its own section sits, so a role can place one between core sections where that reads better. The core order holds among the core sections alone.
|
|
78
80
|
- Keep a section out of the core when a session holding no such role would leave it unfilled. An unfilled section teaches a reader to skip the file.
|
|
81
|
+
- Cite the route onto `## Writing one` rather than restating the procedure, so the extension is the only thing the adding surface carries and the two cannot fall out of step
|
|
79
82
|
- Carry a resume instruction inside the map itself when the routing to that instruction is what a compaction takes. The map survives what the instruction was loaded from, so each reaches a reader the other cannot.
|
|
80
83
|
|
|
81
84
|
## Template
|
package/standards/slug.md
CHANGED
|
@@ -29,3 +29,11 @@ The empty result is a detached HEAD, and the surface picks one of three response
|
|
|
29
29
|
- Fall back to `latest`, so a read-only pass still writes somewhere predictable
|
|
30
30
|
- Stop, when the surface commits or opens a pull request. There is no branch to put the work on, so `latest` would bury the problem instead of reporting it. State the stop in the surface's guards.
|
|
31
31
|
- Fall through to the next source, when the slug is one candidate among several rather than the name of an output file
|
|
32
|
+
|
|
33
|
+
## Changing the transform
|
|
34
|
+
|
|
35
|
+
A change here moves what the transform produces, and every declaration asserting the old output keeps asserting it. Nothing reports those, because a fixture carrying an old filename names neither this file nor the surface that reads it, so a diff over the change reaches none of them. Dropping the type segment is the change that proved it: four fixtures and three scenario scripts kept the type-carrying spelling, one surfaced as a red arm months later, and the other six were found by deriving from the rule.
|
|
36
|
+
|
|
37
|
+
Run `aitk gov superseded <old> <new>` in the same change, with the old output and the new one as the two arguments. The value is the only key both sides carry, and the sweep reads the whole tracked tree rather than a path someone predicted. Its output is a reading rather than a verdict, since a string appears for reasons unrelated to any convention, and a declaration disagreeing on purpose carries an `aitk-allow-superseded: <reason>` marker on its own line or the one above.
|
|
38
|
+
|
|
39
|
+
The sweep sees a value and nothing else. A prose reference that went stale without carrying it, such as a passage citing the wrong standard for the transform, is reached by reading rather than by any key.
|