@erclx/canon 4.16.0 → 4.17.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/package.json +1 -1
- package/src/gate/measures.ts +88 -0
- package/src/gate/stages.ts +12 -0
package/package.json
CHANGED
package/src/gate/measures.ts
CHANGED
|
@@ -164,6 +164,94 @@ export const unreferencedRules: Measure = async (ctx) => {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The files the sweep would rewrite, each with its own count, taken from the
|
|
169
|
+
* `paths` array the record carries beside the total.
|
|
170
|
+
*
|
|
171
|
+
* Every field is tested rather than trusted. The record reaches here as parsed
|
|
172
|
+
* JSON rather than as a type the compiler checked, so a shape that moved
|
|
173
|
+
* upstream drops the entries it can no longer read and leaves the count that
|
|
174
|
+
* was read from a field of its own standing.
|
|
175
|
+
*/
|
|
176
|
+
function citedPaths(record: { paths?: unknown } | undefined): string[] {
|
|
177
|
+
if (!Array.isArray(record?.paths)) return []
|
|
178
|
+
|
|
179
|
+
return record.paths.flatMap((entry) => {
|
|
180
|
+
const cited = entry as { path?: unknown; rewritten?: unknown }
|
|
181
|
+
if (typeof cited.path !== 'string') return []
|
|
182
|
+
return typeof cited.rewritten === 'number'
|
|
183
|
+
? [`${cited.path} (${cited.rewritten})`]
|
|
184
|
+
: [cited.path]
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A second run of the records move should rewrite nothing, and the count is
|
|
190
|
+
* only knowable once the folders themselves have landed.
|
|
191
|
+
*
|
|
192
|
+
* `moves` empty means every record folder already sits at `.canon/`, so any
|
|
193
|
+
* citation the sweep would still rewrite is one the move left stale, which is
|
|
194
|
+
* the defect this stage exists to catch. Where `moves` is nonempty the tree has
|
|
195
|
+
* not migrated at all and a nonzero rewrite count is the verb describing its
|
|
196
|
+
* own first pass, so the reading is reported and never failed on.
|
|
197
|
+
*
|
|
198
|
+
* Exit `0` is a tree with nothing to do and exit `2` is a plan drawn without
|
|
199
|
+
* `--write`, so both read a tree and both carry a record. Every other exit is a
|
|
200
|
+
* refusal that planned nothing, which is unmeasured for the reason the markdown
|
|
201
|
+
* stage treats its own refusal exit so.
|
|
202
|
+
*/
|
|
203
|
+
export const recordIdempotence: Measure = async (ctx) => {
|
|
204
|
+
const run = await ctx.cli(['migrate', 'records', '--json'])
|
|
205
|
+
|
|
206
|
+
if (run.exitCode !== 0 && run.exitCode !== 2) {
|
|
207
|
+
return {
|
|
208
|
+
emissions: [],
|
|
209
|
+
unmeasured: `The records sweep refused (exit ${run.exitCode}) and planned nothing.`,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const record = parseJson(run.stdout) as
|
|
214
|
+
| { moves?: unknown; rewritten?: unknown; paths?: unknown }
|
|
215
|
+
| undefined
|
|
216
|
+
const moves = record?.moves
|
|
217
|
+
const rewritten = record?.rewritten
|
|
218
|
+
|
|
219
|
+
if (!Array.isArray(moves) || typeof rewritten !== 'number') {
|
|
220
|
+
return {
|
|
221
|
+
emissions: [],
|
|
222
|
+
unmeasured:
|
|
223
|
+
'The records sweep carried no plan, so the stage read no count. Run bun src/cli.ts migrate records --json.',
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (moves.length > 0) {
|
|
228
|
+
return {
|
|
229
|
+
emissions: [
|
|
230
|
+
info(
|
|
231
|
+
`${moves.length} record folder(s) still at the old root, with ${rewritten} citation(s) that move with them`,
|
|
232
|
+
),
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (rewritten > 0) {
|
|
238
|
+
return {
|
|
239
|
+
// The payload already names every file, so listing them here is what
|
|
240
|
+
// separates a count a reader has to go and reproduce from a remedy they
|
|
241
|
+
// can act on. A malformed `paths` costs the list and not the finding,
|
|
242
|
+
// since the count above it was read from a field of its own.
|
|
243
|
+
emissions: citedPaths(record).map((path) => warn(path)),
|
|
244
|
+
failure: `The records are at .canon/ and ${rewritten} citation(s) still name the old root, so a second run of canon migrate records would rewrite them. Repoint each one, or mark it canon-keep-record-root where the sentence has to keep the old spelling.`,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
emissions: [
|
|
250
|
+
info('Records at .canon/, and a re-run of the move rewrites nothing'),
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
167
255
|
/**
|
|
168
256
|
* A banned character, word, or spelling is a fact rather than a threshold, so
|
|
169
257
|
* it fails the push while bullet, paragraph, and depth weight stay advisory.
|
package/src/gate/stages.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
markdownBans,
|
|
5
5
|
type Measure,
|
|
6
6
|
pluginManifests,
|
|
7
|
+
recordIdempotence,
|
|
7
8
|
sandboxCoverage,
|
|
8
9
|
seedStandards,
|
|
9
10
|
standardCriteria,
|
|
@@ -337,6 +338,17 @@ export const STAGES: readonly Stage[] = [
|
|
|
337
338
|
],
|
|
338
339
|
success: 'Rule citations resolve',
|
|
339
340
|
},
|
|
341
|
+
{
|
|
342
|
+
// A citation naming the old record root resolves to nothing once the
|
|
343
|
+
// folders have moved, and the sweep that would repoint it only runs when a
|
|
344
|
+
// person calls it. Reading its plan back is what turns a silent stale
|
|
345
|
+
// citation into a stopped push, and it sits with the two citation stages
|
|
346
|
+
// above rather than at the end of the file because it answers their
|
|
347
|
+
// question about a root rather than a path.
|
|
348
|
+
id: 'record-idempotence',
|
|
349
|
+
label: 'Record idempotence',
|
|
350
|
+
checks: [{ kind: 'measure', measure: recordIdempotence }],
|
|
351
|
+
},
|
|
340
352
|
{
|
|
341
353
|
id: 'markdown-bans',
|
|
342
354
|
label: 'Markdown bans',
|