@rungs/cli 0.1.2 → 0.2.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/modules/README.md CHANGED
@@ -91,6 +91,19 @@ whether its guard has ever actually fired.
91
91
  `module.toml`, per skill, with the portability cost stated there.
92
92
  5. **Every gate declares a self-test asserting both directions.** A gate whose rules are currently
93
93
  satisfied is indistinguishable from a gate that matches nothing.
94
+ 5b. **Every gate declares its `applicability`**, next to its `engine`, and there is no default:
95
+
96
+ | Value | Means | Runs on a repo that is not ours? |
97
+ | --- | --- | --- |
98
+ | `repo-content` | Measures the repo's own content — a count, a length, whether a link resolves | **Yes** |
99
+ | `our-artifacts` | Checks something rungs wrote, which cannot exist unless we installed | No |
100
+ | `our-schema` | Reads their file against a shape we defined | No |
101
+
102
+ `doctor --explain` runs only `repo-content` against a repo that has its own equivalent of a
103
+ module. Omitting the field does not mean "safe": the gate is skipped **and named**, and
104
+ `rungs modules` refuses the manifest. This exists because the first version of `--explain`
105
+ decided it centrally by engine name, and produced 71 findings that were true about our
106
+ conventions and meaningless about the repos they landed on.
94
107
  6. **`[detect]` must correctly classify all four source repos.** That is the Phase 6 acceptance
95
108
  criterion, and it is why detection is biased toward false negatives.
96
109
  7. **A managed-block marker uses the target file's comment syntax** — `<!-- rungs:begin x -->` in
@@ -23,6 +23,10 @@ enum = { status = ["proposed", "accepted", "rejected", "superseded"] }
23
23
  # supersession leaves a reader on the stale record with no way to find the live one.
24
24
  [frontmatter_schema.reciprocal]
25
25
  pairs = [{ from = "superseded_by", to = "supersedes" }]
26
+ # A record cannot declare itself superseded without saying by what. Stated
27
+ # separately from `pairs` because it is a different failure: `pairs` catches a
28
+ # one-way link, this catches a missing one.
29
+ required_when = { superseded = "superseded_by" }
26
30
 
27
31
  # Sections, not prose quality. The gate can check that `Alternatives considered` exists and is not
28
32
  # empty; it cannot judge whether the alternatives were real, and one that tried would be the
@@ -40,12 +44,12 @@ done rather than a decision — which is what admission criterion 2 is guarding
40
44
  # ── self-tests ────────────────────────────────────────────────────────────────
41
45
 
42
46
  [[self_test]]
43
- gate = "adr-required-fields"
47
+ gate = "adr-sections-present"
44
48
  expect = "fail"
45
49
  fixture = { file = "{{path}}/{{id_prefix}}-0001-x.md", sections = ["Context", "Decision", "Consequences"] }
46
50
 
47
51
  [[self_test]]
48
- gate = "adr-required-fields"
52
+ gate = "adr-sections-present"
49
53
  expect = "pass"
50
54
  fixture = { file = "{{path}}/{{id_prefix}}-0001-x.md", sections = ["Context", "Decision", "Consequences", "Alternatives considered", "Revisit triggers"] }
51
55
 
@@ -54,6 +58,14 @@ gate = "adr-required-fields"
54
58
  expect = "fail"
55
59
  fixture = { frontmatter = { id = "{{id_prefix}}-0001", title = "x", status = "superseded", date = "2026-01-01" } }
56
60
 
61
+ # The pass direction was missing entirely: both of this gate's fixtures described *sections* and
62
+ # were relabelled to `adr-sections-present` (F-018), which left it asserting only that something
63
+ # fails. A gate with no passing fixture cannot tell "correct" from "matches nothing".
64
+ [[self_test]]
65
+ gate = "adr-required-fields"
66
+ expect = "pass"
67
+ fixture = { frontmatter = { id = "{{id_prefix}}-0001", title = "x", status = "accepted", date = "2026-01-01" } }
68
+
57
69
  [[self_test]]
58
70
  gate = "adr-index-current"
59
71
  expect = "fail"
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "adr"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  rung = 1
5
5
  summary = "Numbered, immutable decision records, with an admission rule that keeps the directory from becoming a second documentation tree."
6
6
 
@@ -27,14 +27,32 @@ default = "ADR"
27
27
  id = "adr-index-current"
28
28
  kind = "declared"
29
29
  engine = "render-freshness"
30
+ applicability = "our-artifacts"
30
31
  table = "gates/adr.toml"
31
32
  tier = "fast"
32
33
  why = "An index that has stopped listing every record is a routing table that silently drops decisions."
33
34
 
35
+ [[gates]]
36
+ id = "adr-sections-present"
37
+ kind = "declared"
38
+ engine = "sections"
39
+ applicability = "our-schema"
40
+ table = "gates/adr.toml"
41
+ tier = "fast"
42
+ why = """
43
+ The `[sections]` table required Context, Decision, Consequences, Alternatives considered and
44
+ Revisit triggers with non-empty bodies, and **no gate declared it** — a fully specified check that
45
+ nothing ran, found 2026-08-16 by executing the self-test fixtures (F-018). Two fixtures were even
46
+ labelled for `adr-required-fields`, which checks frontmatter and could never have satisfied them.
47
+ A record without alternatives is a description of what was done rather than a decision, which is
48
+ what the admission rule's second criterion guards against.
49
+ """
50
+
34
51
  [[gates]]
35
52
  id = "adr-required-fields"
36
53
  kind = "declared"
37
54
  engine = "frontmatter-schema"
55
+ applicability = "our-schema"
38
56
  table = "gates/adr.toml"
39
57
  tier = "fast"
40
58
  why = """
@@ -28,6 +28,7 @@ default = "docs/audit-criteria.md"
28
28
  id = "audit-output-is-rows"
29
29
  kind = "declared"
30
30
  engine = "file-population"
31
+ applicability = "repo-content"
31
32
  table = "gates/audit.toml"
32
33
  tier = "fast"
33
34
  why = """
@@ -104,3 +104,36 @@ fixture = { items = ["{{id_prefix}}-001-a.md", "{{id_prefix}}-002-b.md"] }
104
104
  gate = "backlog-ids"
105
105
  expect = "fail"
106
106
  fixture = { items = ["{{id_prefix}}-001-a.md"], marker = "{{id_prefix}}-001" }
107
+
108
+ # ── backlog-board-reconciled ──────────────────────────────────────────────────
109
+ #
110
+ # The board groups rows by status; each item declares its own. Nothing checked
111
+ # that the two agreed, and on 2026-08-16 fourteen rows disagreed — nine under
112
+ # `Proposed` and five under `Planned`, all naming files whose status was `done`,
113
+ # nine of them already moved into `archive/`.
114
+ #
115
+ # The heading map is data, not code, because a repo may rename a column or drop
116
+ # one. An undeclared heading is reported rather than skipped: a row that hides
117
+ # from the check is exactly the case this exists for.
118
+
119
+ [board_reconcile]
120
+ file = "docs/{{root}}/BACKLOG.md"
121
+ exempt_marker = "board-ok:" # ignored unless it states a reason
122
+
123
+ [board_reconcile.groups]
124
+ "In progress" = ["in_progress"]
125
+ "Review" = ["review"]
126
+ "Planned" = ["planned"]
127
+ "Accepted" = ["accepted"]
128
+ "Proposed" = ["proposed"]
129
+ "Deferred" = ["deferred"]
130
+
131
+ [[self_test]]
132
+ gate = "backlog-board-reconciled"
133
+ expect = "fail"
134
+ input = "## Proposed\n| [{{id_prefix}}-001](items/{{id_prefix}}-001-x.md) | done item filed as proposed | docs |"
135
+
136
+ [[self_test]]
137
+ gate = "backlog-board-reconciled"
138
+ expect = "pass"
139
+ input = "## Proposed\n| [{{id_prefix}}-001](items/{{id_prefix}}-001-x.md) | a genuinely proposed item | docs |"
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "backlog"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  rung = 1
5
5
  summary = "Work items with stable ids, an eight-status lifecycle, a board, and the three gates that keep the bookkeeping honest."
6
6
 
@@ -42,6 +42,7 @@ default = "feature"
42
42
  id = "backlog-ids"
43
43
  kind = "declared"
44
44
  engine = "id-integrity"
45
+ applicability = "our-schema"
45
46
  table = "gates/ids.toml"
46
47
  tier = "fast"
47
48
  why = "An id claimed twice is two items the board shows as one."
@@ -50,6 +51,7 @@ why = "An id claimed twice is two items the board shows as one."
50
51
  id = "backlog-stale-blocker"
51
52
  kind = "declared"
52
53
  engine = "id-integrity"
54
+ applicability = "our-schema"
53
55
  table = "gates/ids.toml"
54
56
  tier = "fast"
55
57
  why = """
@@ -62,6 +64,7 @@ finished item as next owner, then found those routes quoted in a code comment as
62
64
  id = "backlog-merged-status"
63
65
  kind = "declared"
64
66
  engine = "git-status-reconcile"
67
+ applicability = "our-schema"
65
68
  table = "gates/ids.toml"
66
69
  tier = "fast"
67
70
  why = """
@@ -70,6 +73,22 @@ rift-forge: 37 items sat at `review` with their code already landed, and one sat
70
73
  for eight days after its own merge.
71
74
  """
72
75
 
76
+ [[gates]]
77
+ id = "backlog-board-reconciled"
78
+ kind = "declared"
79
+ engine = "board-reconcile"
80
+ applicability = "our-artifacts"
81
+ table = "gates/ids.toml"
82
+ tier = "fast"
83
+ why = """
84
+ The board groups rows by status and each item declares its own; nothing checked that the two agreed.
85
+ Measured here 2026-08-16: fourteen rows disagreed — nine under `Proposed`, five under `Planned`, all
86
+ naming items whose files read `status: done`, and nine of them already moved into `archive/`, so the
87
+ board called work proposed while pointing at the directory for work that cannot change. An outside
88
+ reader checking whether that work was done would have been told no. `backlog-merged-status`
89
+ reconciles a branch against the same field; this is the layer above it.
90
+ """
91
+
73
92
  # ── skills ────────────────────────────────────────────────────────────────────
74
93
  # SKILL.md files stay restricted to the six Agent Skills spec fields (ADR-0001).
75
94
  # Claude Code extensions are opted into here, per skill, with the cost stated.
@@ -110,6 +129,47 @@ coexist — a milestone overlay sits on top of items. Three real options: adopt
110
129
  unit and install only the missing gates, install items alongside, or skip.
111
130
  """
112
131
 
132
+ # The repo's work lives in an issue tracker. Without this, a team running
133
+ # everything through GitHub Issues matched no backlog signature at all, was
134
+ # reported `absent`, and `doctor` proposed installing a Markdown backlog beside
135
+ # the one they actually use — which is exactly the "why isn't this Jira?"
136
+ # objection, earned.
137
+ #
138
+ # **Detection only, and deliberately so.** An adapter that read or wrote an
139
+ # external tracker would need credentials, network and a client inside the
140
+ # consumer repo, which ADR-0002 rules out. ADR-0004 already answers the
141
+ # ownership worry the right way: adoption is a mapping, not a migration. So
142
+ # rungs' job here is to notice and stop, not to sync.
143
+ #
144
+ # Only configured issue *intake* counts. A `.github/` directory does not — 76
145
+ # local repos have one and none of them run their work in Issues (measured
146
+ # 2026-08-16). An `ISSUE_TEMPLATE` directory or a root issue template is
147
+ # somebody deliberately configuring how work arrives, which is the narrowest
148
+ # in-repo evidence that the tracker is the unit of work.
149
+ #
150
+ # Linear and Jira are **not** listed. They leave no reliable file-level trace —
151
+ # usually nothing but a URL in a PR template — and a signature guessed from a
152
+ # link is the confidently-wrong probe this repo refuses everywhere else.
153
+ # The note covers them because the trade-off does not depend on the vendor.
154
+ [[detect.paradigm]]
155
+ id = "external-tracker"
156
+ paths = [
157
+ ".github/ISSUE_TEMPLATE/**",
158
+ ".github/issue_template.md",
159
+ ".github/ISSUE_TEMPLATE.md",
160
+ ]
161
+ compare = "docs/research/synthesis.md#33-unit-of-work"
162
+ note = """
163
+ Work items here are Markdown files in the repo; your tracker is a system outside it. Neither is
164
+ better — they fail differently. A tracker gives you queries, notifications and non-engineers; files
165
+ give you review, history and offline agents, and they are the only form a gate can mechanically
166
+ check against a branch.
167
+
168
+ rungs will not sync, import or export. Three real options: keep the tracker as the unit of work and
169
+ install only the modules that do not duplicate it (`findings`, `adr`, `session`), install items
170
+ alongside for agent-facing work while humans stay in the tracker, or skip the module.
171
+ """
172
+
113
173
  # Proposes a parameter once presence is already established. Never concludes
114
174
  # presence — hexguard-templates has 207 well-formed `FOUND-US-###` matches and
115
175
  # no backlog; they are spec story ids.
@@ -42,6 +42,7 @@ default = ".github/workflows/checks.yml"
42
42
  id = "ci-workflow-proliferation"
43
43
  kind = "declared"
44
44
  engine = "file-population"
45
+ applicability = "repo-content"
45
46
  table = "gates/ci.toml"
46
47
  tier = "fast"
47
48
  why = """
@@ -45,6 +45,7 @@ default = "integ/"
45
45
  id = "concurrency-no-integration-checkout"
46
46
  kind = "declared"
47
47
  engine = "git-state"
48
+ applicability = "our-schema"
48
49
  table = "gates/concurrency.toml"
49
50
  tier = "fast"
50
51
  why = """
@@ -59,6 +60,7 @@ releases the branch mid-run. Two land processes were measured running at once.
59
60
  id = "concurrency-generated-not-text-merged"
60
61
  kind = "declared"
61
62
  engine = "merge-driver-check"
63
+ applicability = "our-artifacts"
62
64
  table = "gates/concurrency.toml"
63
65
  tier = "fast"
64
66
  why = """
@@ -31,6 +31,7 @@ default = "docs/design-requests"
31
31
  id = "design-deltas-routed"
32
32
  kind = "declared"
33
33
  engine = "register-schema"
34
+ applicability = "our-schema"
34
35
  table = "gates/design.toml"
35
36
  tier = "fast"
36
37
  why = """
@@ -44,6 +45,7 @@ they are aligned.
44
45
  id = "design-mirror-not-edited"
45
46
  kind = "declared"
46
47
  engine = "render-freshness"
48
+ applicability = "our-artifacts"
47
49
  table = "gates/design.toml"
48
50
  tier = "fast"
49
51
  why = "A hand-edit in the mirror is a local change that the next pull silently destroys, and until then it is a fact about the design system that the design system does not know."
@@ -38,6 +38,7 @@ default = true
38
38
  id = "docauth-ownership-respected"
39
39
  kind = "declared"
40
40
  engine = "term-ownership"
41
+ applicability = "our-schema"
41
42
  table = "gates/authority.toml"
42
43
  tier = "fast"
43
44
  why = """
@@ -50,6 +51,7 @@ because the failure it catches is a whole section restated in the wrong file, no
50
51
  id = "docauth-scope-headers"
51
52
  kind = "declared"
52
53
  engine = "sections"
54
+ applicability = "our-schema"
53
55
  table = "gates/authority.toml"
54
56
  tier = "fast"
55
57
  why = "A mandatory rule with a zero-cost check has no excuse to be manual, and this one was manual in the repo that called it non-negotiable."
@@ -58,6 +60,7 @@ why = "A mandatory rule with a zero-cost check has no excuse to be manual, an
58
60
  id = "docauth-working-rules"
59
61
  kind = "declared"
60
62
  engine = "rule-propagation"
63
+ applicability = "our-schema"
61
64
  table = "gates/authority.toml"
62
65
  tier = "fast"
63
66
  why = """
@@ -71,6 +74,7 @@ changes, and reads as verified precisely because it names a source.
71
74
  id = "docauth-no-redirect-stubs"
72
75
  kind = "declared"
73
76
  engine = "file-population"
77
+ applicability = "repo-content"
74
78
  table = "gates/authority.toml"
75
79
  tier = "fast"
76
80
  why = "A file that exists only to say 'go elsewhere' is a bug: it will be cited, and the citation will look authoritative."
@@ -30,6 +30,7 @@ default = "docs/{{backlog.root}}/FINDINGS.md"
30
30
  id = "findings-ids"
31
31
  kind = "declared"
32
32
  engine = "id-integrity"
33
+ applicability = "our-schema"
33
34
  table = "gates/findings.toml"
34
35
  tier = "fast"
35
36
  why = "Two findings sharing an id are one finding as far as every citation is concerned."
@@ -38,6 +39,7 @@ why = "Two findings sharing an id are one finding as far as every citation is
38
39
  id = "findings-disposition-has-reason"
39
40
  kind = "declared"
40
41
  engine = "register-schema"
42
+ applicability = "our-schema"
41
43
  table = "gates/findings.toml"
42
44
  tier = "fast"
43
45
  why = """
@@ -49,6 +51,7 @@ is the same observation the next session will record again. The reason is what t
49
51
  id = "findings-self-declared-closure"
50
52
  kind = "declared"
51
53
  engine = "self-declared-closure"
54
+ applicability = "our-schema"
52
55
  table = "gates/findings.toml"
53
56
  tier = "fast"
54
57
  why = "An open finding whose own detail says it is fixed contradicts the register; this narrow text check catches drift without pretending to prove code-level closure."
@@ -5,7 +5,13 @@
5
5
 
6
6
  # ── gates-links-resolve · gates-paths-exist ───────────────────────────────────
7
7
 
8
- [link_integrity]
8
+ # Two entries, one per gate. They were a single table shared by both gate ids, which ran the
9
+ # identical markdown-link scan twice and reported every finding twice — 224 lines for 112 broken
10
+ # links on one repo (F-007). Splitting them also lets `gates-paths-exist` do the job its `why`
11
+ # actually describes: hexguard's instruction files name paths in code spans, not in markdown links.
12
+ [[link_integrity]]
13
+ id = "gates-links-resolve"
14
+ check = ["relative_markdown_links"]
9
15
  scan = ["**/*.md", ".ai/**/*.toml"]
10
16
  # `modules/**` is a rungs module source tree, not repo content: a fragment's
11
17
  # links are relative to the file it merges *into*, not to where it is authored,
@@ -13,14 +19,23 @@ scan = ["**/*.md", ".ai/**/*.toml"]
13
19
  # repo nothing — it has no `modules/` — and was found by running this against
14
20
  # the rungs repo itself.
15
21
  exclude = ["node_modules/**", "**/archive/**", ".git/**", "modules/*/fragments/**", "modules/*/files/**"]
16
- check = ["relative_markdown_links", "backticked_paths"]
17
- # Backticked paths are checked only when they look like paths — containing a `/` or a known
18
- # extension — because prose is full of `identifiers` that are not files. Under-detection is the
19
- # correct bias: a missed stale path is a nuisance, a false refusal on every code span is a gate
20
- # people delete.
21
- path_hint = ["/", ".md", ".ts", ".cs", ".mjs", ".toml", ".json", ".ps1"]
22
22
  exempt = "path-ok:" # ignored unless it states a reason
23
23
 
24
+ [[link_integrity]]
25
+ id = "gates-paths-exist"
26
+ check = ["backticked_paths"]
27
+ # Instruction files, where a named path going stale is the measured incident. Deliberately not
28
+ # every markdown file: prose about a repo cites paths that were true when written, and a gate that
29
+ # refuses history is one people delete.
30
+ scan = ["AGENTS.md", "CLAUDE.md", ".ai/**/*.md", ".ai/**/*.toml", ".github/instructions/**/*.md"]
31
+ exclude = ["node_modules/**", "**/archive/**", ".git/**"]
32
+ # Checked only when it looks like a path — containing a `/` or a known extension — because prose is
33
+ # full of `identifiers` that are not files. Resolved from the repo root, which is what an
34
+ # instruction file's reader does. Under-detection is the correct bias: a missed stale path is a
35
+ # nuisance, a false refusal on every code span is a gate people delete.
36
+ path_hint = ["/", ".md", ".ts", ".cs", ".mjs", ".toml", ".json", ".ps1"]
37
+ exempt = "path-ok:"
38
+
24
39
  [link_integrity.notes]
25
40
  external = "not fetched — a gate that needs the network is a gate that fails on a plane"
26
41
 
@@ -84,11 +99,39 @@ gate = "gates-links-resolve"
84
99
  expect = "pass"
85
100
  input = "The module writes [its readme](${{path}}/README.md) once installed."
86
101
 
102
+ # WI-042. `path/file.ts:387` is a code reference, not a broken link — it is the form CLAUDE.md
103
+ # mandates. Resolved literally it reported 1,794 false findings on rift-forge, 46.6% of that repo's
104
+ # link findings. Both directions, because the strip must narrow what is reported and never widen it:
105
+ # an existing file with a line number passes, a missing one with a line number still fails.
106
+ [[self_test]]
107
+ gate = "gates-links-resolve"
108
+ expect = "pass"
109
+ input = "The store is built in [forge-store](./structural.toml:222)."
110
+
111
+ [[self_test]]
112
+ gate = "gates-links-resolve"
113
+ expect = "fail"
114
+ input = "The store is built in [forge-store](./no-such-file.toml:222)."
115
+
87
116
  [[self_test]]
88
117
  gate = "gates-paths-exist"
89
118
  expect = "pass"
90
119
  input = "The `Result<T>` type wraps it." # a code span that is not a path
91
120
 
121
+ # F-007. `backticked_paths` was declared in the table's `check` list and implemented nowhere, so
122
+ # this gate silently ran the markdown-link scan instead and double-reported it. Implemented now,
123
+ # and narrowed by measurement: the first run produced ten findings on this repo and all ten were
124
+ # wrong — slash commands, `WI-###` placeholders, illustrative directories, bare filenames.
125
+ [[self_test]]
126
+ gate = "gates-paths-exist"
127
+ expect = "fail"
128
+ input = "Claim an id from `docs/backlog/DOES-NOT-EXIST.md` first."
129
+
130
+ [[self_test]]
131
+ gate = "gates-paths-exist"
132
+ expect = "pass"
133
+ input = "Run `/work-item`, branch `feature/WI-###-slug`, and read `.cursor/rules/`."
134
+
92
135
  [[self_test]]
93
136
  gate = "gates-rules-declare-enforcement"
94
137
  expect = "fail"
@@ -99,10 +142,20 @@ gate = "gates-rules-declare-enforcement"
99
142
  expect = "pass"
100
143
  fixture = { file = ".ai/rules/x.md", frontmatter = { description = "d", enforcement = "review-only" } }
101
144
 
145
+ # F-018. This gate's three fixtures all described `.claude/skills/**/SKILL.md`, and the skill
146
+ # frontmatter schema moved to the `skills` module — so they asserted a schema this table no longer
147
+ # contains and could never pass or fail for the stated reason. They were left behind by the move and
148
+ # nobody noticed, because no fixture had ever been executed. Replaced with the schema that is
149
+ # actually here.
102
150
  [[self_test]]
103
151
  gate = "gates-frontmatter-valid"
104
152
  expect = "fail"
105
- fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { name = "x", description = "d", "argument-hint" = "y" } }
153
+ fixture = { file = ".ai/rules/x.md", frontmatter = { enforcement = "gated" } }
154
+
155
+ [[self_test]]
156
+ gate = "gates-frontmatter-valid"
157
+ expect = "pass"
158
+ fixture = { file = ".ai/rules/x.md", frontmatter = { description = "d", enforcement = "gated" } }
106
159
 
107
160
  [[self_test]]
108
161
  gate = "gates-self-tests-both-directions"
@@ -130,12 +183,3 @@ gate = "gates-paths-exist"
130
183
  expect = "pass"
131
184
  input = "The mapper lives in [`structural.toml`](./structural.toml)."
132
185
 
133
- [[self_test]]
134
- gate = "gates-frontmatter-valid"
135
- expect = "pass"
136
- fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { name = "x", description = "d" } }
137
-
138
- [[self_test]]
139
- gate = "gates-frontmatter-valid"
140
- expect = "fail"
141
- fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { description = "d" } }
@@ -39,6 +39,7 @@ default = 30000
39
39
  id = "gates-links-resolve"
40
40
  kind = "declared"
41
41
  engine = "link-integrity"
42
+ applicability = "repo-content"
42
43
  table = "gates/structural.toml"
43
44
  tier = "fast"
44
45
  why = "A relative link that stopped resolving is the cheapest possible signal that a document moved and its citers did not."
@@ -47,6 +48,7 @@ why = "A relative link that stopped resolving is the cheapest possible signal
47
48
  id = "gates-paths-exist"
48
49
  kind = "declared"
49
50
  engine = "link-integrity"
51
+ applicability = "repo-content"
50
52
  table = "gates/structural.toml"
51
53
  tier = "fast"
52
54
  why = "Instruction files name paths. hexguard's seven instruction files describe workspace registration and CI wiring across 105 packages, and nothing detected a moved path."
@@ -55,6 +57,7 @@ why = "Instruction files name paths. hexguard's seven instruction files descr
55
57
  id = "gates-frontmatter-valid"
56
58
  kind = "declared"
57
59
  engine = "frontmatter-schema"
60
+ applicability = "our-schema"
58
61
  table = "gates/structural.toml"
59
62
  tier = "fast"
60
63
  why = "A skill with malformed frontmatter does not fail; it silently never loads."
@@ -63,6 +66,7 @@ why = "A skill with malformed frontmatter does not fail; it silently never lo
63
66
  id = "gates-rules-declare-enforcement"
64
67
  kind = "declared"
65
68
  engine = "frontmatter-schema"
69
+ applicability = "our-schema"
66
70
  table = "gates/structural.toml"
67
71
  tier = "fast"
68
72
  why = """
@@ -75,6 +79,7 @@ not check, and all four decayed — including inside the file that states the ru
75
79
  id = "gates-self-tests-both-directions"
76
80
  kind = "declared"
77
81
  engine = "gate-meta"
82
+ applicability = "our-artifacts"
78
83
  table = "gates/structural.toml"
79
84
  tier = "fast"
80
85
  why = """
@@ -45,6 +45,7 @@ default = 200
45
45
  id = "instructions-core-size"
46
46
  kind = "declared"
47
47
  engine = "file-budget"
48
+ applicability = "repo-content"
48
49
  table = "gates/core.toml"
49
50
  tier = "fast"
50
51
  why = "rift-forge's entry document reached 1513 lines and every session read all of it, in the one repo that otherwise measured everything."
@@ -53,6 +54,7 @@ why = "rift-forge's entry document reached 1513 lines and every session read
53
54
  id = "instructions-render-current"
54
55
  kind = "declared"
55
56
  engine = "render-freshness"
57
+ applicability = "our-artifacts"
56
58
  table = "gates/core.toml"
57
59
  tier = "fast"
58
60
  why = "A rendered rule that no longer matches its source is a rule the agent is not reading. A green check means 'not yet regenerated', never 'current'."
@@ -61,6 +63,7 @@ why = "A rendered rule that no longer matches its source is a rule the agent
61
63
  id = "instructions-repo-map-current"
62
64
  kind = "declared"
63
65
  engine = "render-freshness"
66
+ applicability = "our-artifacts"
64
67
  table = "gates/core.toml"
65
68
  tier = "fast"
66
69
  why = "Every source repo hand-maintained a repo map and one of them grew five duplicated entries — in the file every session reads first."
@@ -72,6 +75,7 @@ why = "Every source repo hand-maintained a repo map and one of them grew five
72
75
  id = "instructions-shell-backticks"
73
76
  kind = "declared"
74
77
  engine = "shell-safety"
78
+ applicability = "our-schema"
75
79
  table = "gates/core.toml"
76
80
  trigger = "pre-tool-use"
77
81
  matcher = "Bash|PowerShell"
@@ -47,6 +47,25 @@ uncovered = [
47
47
  ]
48
48
  note = "A green check means these files agree, not that the version is correct everywhere."
49
49
 
50
+ # ── release-fragment-current ──────────────────────────────────────────────────
51
+ #
52
+ # The other half of the fragment discipline. `release-changelog-fragment` catches a release-worthy
53
+ # change that added no fragment; this catches a fragment that was consumed and never deleted.
54
+ #
55
+ # Only filenames that parse as a version are judged. A repo whose fragments are named per-change
56
+ # (`42.feature.md`) states no version, so there is nothing to compare and nothing is claimed — a
57
+ # gate that refused a naming convention it was not asked about is one people turn off.
58
+
59
+ [[changelog_freshness]]
60
+ id = "release-fragment-current"
61
+ fragments = ["{{changelog_dir}}/*.md"]
62
+ version = { file = "package.json", path = "version" }
63
+ message = """
64
+ This fragment names a version below the one being prepared, so it belongs to a release that already
65
+ shipped. Delete it — fragments are consumed, not archived, and one left behind is republished as
66
+ though it were new work.
67
+ """
68
+
50
69
  # ── self-tests ────────────────────────────────────────────────────────────────
51
70
 
52
71
  [[self_test]]
@@ -74,6 +93,30 @@ gate = "release-changelog-fragment"
74
93
  expect = "fail"
75
94
  fixture = { changed = ["src/a.ts"], fragments = [], exempt = "changelog-ok:" }
76
95
 
96
+ [[self_test]]
97
+ gate = "release-fragment-current"
98
+ expect = "fail"
99
+ fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["0.1.1.md"] }
100
+
101
+ [[self_test]]
102
+ gate = "release-fragment-current"
103
+ expect = "pass"
104
+ fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["0.2.0.md"] }
105
+
106
+ # A fragment for a version *above* the one being prepared is not stale — it is a branch that
107
+ # already decided it is a minor while the package has not been bumped yet.
108
+ [[self_test]]
109
+ gate = "release-fragment-current"
110
+ expect = "pass"
111
+ fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["0.3.0.md"] }
112
+
113
+ # Per-change fragment naming states no version, so there is nothing to compare and nothing is
114
+ # claimed. The gate must stay silent rather than refuse a convention it was not asked about.
115
+ [[self_test]]
116
+ gate = "release-fragment-current"
117
+ expect = "pass"
118
+ fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["42.feature.md"] }
119
+
77
120
  [[self_test]]
78
121
  gate = "release-version-consistent"
79
122
  expect = "fail"
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "release"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  rung = 3
5
5
  summary = "Candidate, stable and deploy branch lines; one procedure for cutting a release, hotfixing and rolling back; changelog fragments instead of a shared file."
6
6
 
@@ -46,6 +46,7 @@ consumed_by = "render"
46
46
  id = "release-changelog-fragment"
47
47
  kind = "declared"
48
48
  engine = "file-population"
49
+ applicability = "repo-content"
49
50
  table = "gates/release.toml"
50
51
  tier = "fast"
51
52
  why = """
@@ -54,10 +55,24 @@ cut time when nobody remembers what it did. Cheaper to require the fragment on t
54
55
  the change.
55
56
  """
56
57
 
58
+ [[gates]]
59
+ id = "release-fragment-current"
60
+ kind = "declared"
61
+ engine = "changelog-freshness"
62
+ applicability = "repo-content"
63
+ table = "gates/release.toml"
64
+ tier = "fast"
65
+ why = """
66
+ Fragments are consumed at release time, not archived — one left behind appears in the next release
67
+ too, where it reads as unreleased work. `cut-release` §3 has said so in prose since it was written,
68
+ and prose did not hold it: this repo's own `0.1.1` fragment survived two releases (F-022).
69
+ """
70
+
57
71
  [[gates]]
58
72
  id = "release-version-consistent"
59
73
  kind = "declared"
60
74
  engine = "computed-claim"
75
+ applicability = "our-schema"
61
76
  table = "gates/release.toml"
62
77
  tier = "fast"
63
78
  why = "A version appears in several files and drifts in exactly one of them. It is derivable, so it is computed rather than typed."
@@ -22,9 +22,16 @@ mismatch is usually a fragment somebody skipped, not a versioning disagreement.
22
22
  ## 2. Gate
23
23
 
24
24
  ```bash
25
- rungs check --tier full
25
+ rungs check
26
26
  ```
27
27
 
28
+ Run **every** registered gate. This said `rungs check --tier full`, which selects only gates
29
+ labelled `full` — on a repo whose gates are all `fast`, that is zero of them, and the run reports
30
+ no gates rather than a pass. A release step that gates on nothing while looking green is worse than
31
+ one that is skipped, because nobody goes back to check it.
32
+
33
+ Narrow to a tier (`rungs check --fast`) only when you know the tier holds what you meant to run.
34
+
28
35
  **Do not proceed on a red gate**, and do not weaken one to get through. A release is exactly the
29
36
  moment the temptation is highest and the cost of yielding is highest.
30
37