@erclx/canon 4.46.0 → 4.48.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-address-review/REQUIREMENT.md +4 -4
- package/claude/skills/claude-address-review/SKILL.md +34 -11
- package/claude/skills/claude-ux-audit/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/commands/migrate.ts +34 -0
- package/src/migrate/records.ts +141 -5
- package/standards/versioning.md +5 -0
|
@@ -25,15 +25,15 @@ A declined finding fails on a third axis, which is where its reason ends up. A w
|
|
|
25
25
|
- Push before replying, so the comment never runs ahead of the code it describes
|
|
26
26
|
- Map every finding to what changed, or to a one-line reason when it is a question or a conscious accept
|
|
27
27
|
- Carry the fact behind a declined finding into the posted reply rather than into the message that answered the dispatch, since both sessions end and the thread is what the next reader opens
|
|
28
|
-
-
|
|
29
|
-
- Say what the run actually did on a rebase-only pass, since a reply mapping findings and a
|
|
28
|
+
- Append the closing confirmation to the reply only when the findings are addressed and every check passes
|
|
29
|
+
- Say what the run actually did on a rebase-only pass, since a reply mapping findings and a closing confirmation claiming they were addressed are both false on a pull request carrying none
|
|
30
30
|
- Scan the reply for banned characters and internal phase labels before posting, since the comment leaves for the remote unchecked
|
|
31
31
|
|
|
32
32
|
## Must not
|
|
33
33
|
|
|
34
|
-
- Merge, or read the closing
|
|
34
|
+
- Merge, or read the closing confirmation as an approval. The author cannot approve their own pull request.
|
|
35
35
|
- Write a review. This skill consumes findings and does not produce them.
|
|
36
|
-
-
|
|
36
|
+
- Append the closing confirmation while a check is failing
|
|
37
37
|
- Reimplement the follow-up push or the doc refresh. Both have owners, and a second copy here drifts from them.
|
|
38
38
|
- Edit silently. A finding answered without a reply leaves the reviewer re-deriving the change from the diff.
|
|
39
39
|
- Answer a finding in the channel alone. A reply that changes what the review concluded is the one the thread has to carry.
|
|
@@ -153,34 +153,57 @@ matches it.
|
|
|
153
153
|
Before posting, run the scan in
|
|
154
154
|
`${CLAUDE_SKILL_DIR}/../../standards/publish.md`
|
|
155
155
|
against the reply. The hook skips `.canon/tmp/`, so this scan is the
|
|
156
|
-
only gate on the published reply. Post it to the PR
|
|
156
|
+
only gate on the published reply. Post it to the PR and capture the posted
|
|
157
|
+
comment's id, since Step 7 edits this exact comment rather than trusting
|
|
158
|
+
whichever one `gh` considers last:
|
|
157
159
|
|
|
158
160
|
```bash
|
|
159
|
-
gh pr comment <number> --body-file .canon/tmp/address-review/reply-<number>.md
|
|
161
|
+
comment_url=$(gh pr comment <number> --body-file .canon/tmp/address-review/reply-<number>.md)
|
|
162
|
+
echo "${comment_url##*issuecomment-}" > .canon/tmp/address-review/reply-<number>.id
|
|
160
163
|
```
|
|
161
164
|
|
|
162
165
|
## Step 7: confirm resolution
|
|
163
166
|
|
|
164
167
|
After the follow-up push, watch CI on the PR. Poll
|
|
165
168
|
`canon pr checks <number> --json` until the record's `state` leaves `pending`,
|
|
166
|
-
then read it. When every finding is addressed and the state is `passing`,
|
|
167
|
-
|
|
169
|
+
then read it. When every finding is addressed and the state is `passing`, append
|
|
170
|
+
the closing confirmation to the reply file Step 6 already posted, so the thread
|
|
171
|
+
carries one terminal state rather than a second comment under no heading:
|
|
168
172
|
|
|
169
173
|
```bash
|
|
170
|
-
|
|
174
|
+
printf '\n✅ All review findings addressed, CI green.\n' >> .canon/tmp/address-review/reply-<number>.md
|
|
171
175
|
```
|
|
172
176
|
|
|
173
|
-
A rebase-only run addressed no finding, so it
|
|
174
|
-
|
|
177
|
+
A rebase-only run addressed no finding, so it appends its own confirmation
|
|
178
|
+
instead of that one. Claiming findings were addressed on a pull request that
|
|
175
179
|
carries none is false on a surface nothing else checks:
|
|
176
180
|
|
|
177
181
|
```bash
|
|
178
|
-
|
|
182
|
+
printf '\n✅ Rebased onto origin/main, CI green. No review findings were open.\n' >> .canon/tmp/address-review/reply-<number>.md
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Re-run the `${CLAUDE_SKILL_DIR}/../../standards/publish.md` scan against the
|
|
186
|
+
updated file, since the appended line is new content the Step 6 scan never saw.
|
|
187
|
+
Then edit the exact comment Step 6 posted, read back from the id it saved,
|
|
188
|
+
rather than posting a second comment:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
gh api -X PATCH "repos/{owner}/{repo}/issues/comments/$(cat .canon/tmp/address-review/reply-<number>.id)" \
|
|
192
|
+
-F body=@.canon/tmp/address-review/reply-<number>.md
|
|
179
193
|
```
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
`--edit-last` was the first shape and it targets the wrong object here.
|
|
196
|
+
It edits the last comment posted by the authenticated user, not the last
|
|
197
|
+
comment this run posted, and every session here authenticates as the one
|
|
198
|
+
account the operator also comments from. A comment the operator leaves on
|
|
199
|
+
their own pull request during the minutes Step 7 spends waiting on CI becomes
|
|
200
|
+
that last comment, so `--edit-last` would overwrite it with the reply and the
|
|
201
|
+
confirmation, silently. Targeting the id Step 6 captured closes that whether
|
|
202
|
+
or not anyone commented in between.
|
|
203
|
+
|
|
204
|
+
If any check fails, skip the edit. Report the failing check so it can be fixed
|
|
205
|
+
first. This is a resolution signal, not a formal approval, since the PR author
|
|
206
|
+
cannot approve their own PR.
|
|
184
207
|
|
|
185
208
|
## Step 8: output
|
|
186
209
|
|
|
@@ -16,7 +16,7 @@ Read these in parallel from the project root, skipping any that do not exist:
|
|
|
16
16
|
|
|
17
17
|
- `CLAUDE.md`: project type and conventions
|
|
18
18
|
- `.claude/DESIGN.md`: visual intent, tokens, typography, spacing rules
|
|
19
|
-
- `.claude/wireframes/index.md` + every `.claude/wireframes/<surface>.md
|
|
19
|
+
- `.claude/wireframes/index.md` + every `.claude/wireframes/<surface>.md`, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists: intended UI layout, UI copy, interaction rules. Surfaces are loaded one file at a time during Step 3. Per `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`, these carry layout and intent, not implementation detail.
|
|
20
20
|
- `.claude/REQUIREMENTS.md`: feature scope and non-goals
|
|
21
21
|
|
|
22
22
|
## Step 2: identify surfaces
|
|
@@ -32,7 +32,7 @@ For each surface, look for:
|
|
|
32
32
|
3. **Inconsistencies**: spacing, tone of voice, interaction patterns, icon use, keyboard affordances
|
|
33
33
|
4. **Roughness in daily use**: friction, redundant steps, unclear affordances, ambiguous labels
|
|
34
34
|
|
|
35
|
-
Use `.claude/DESIGN.md` and the per-surface `.claude/wireframes/<surface>.md` files as ground truth for intent. For each implementation surface, read the matching wireframe file before flagging drift. Observations only, no implementation suggestions or fixes.
|
|
35
|
+
Use `.claude/DESIGN.md` and the per-surface `.claude/wireframes/<surface>.md` files, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists, as ground truth for intent. For each implementation surface, read the matching wireframe file before flagging drift. Observations only, no implementation suggestions or fixes.
|
|
36
36
|
|
|
37
37
|
## Step 4: report and persist
|
|
38
38
|
|
package/package.json
CHANGED
package/src/commands/migrate.ts
CHANGED
|
@@ -295,6 +295,31 @@ function reportRecords(plan: RecordsPlan, records: number): void {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
|
|
299
|
+
// Named individually, same reasoning as the glob report above: an ownership
|
|
300
|
+
// boundary and a tense judgment stop being visible the moment either is
|
|
301
|
+
// rewritten, so the reader gets the file and line rather than a count.
|
|
302
|
+
if (plan.crossRepoCitations.length > 0) {
|
|
303
|
+
logInfo(
|
|
304
|
+
`${plural(plan.crossRepoCitations.length, 'file')} carry a citation into another repository, left alone:`,
|
|
305
|
+
)
|
|
306
|
+
for (const entry of plan.crossRepoCitations) {
|
|
307
|
+
for (const line of entry.lines) {
|
|
308
|
+
logInfo(` ${entry.path}:${line.line} ${excerpt(line.text)}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (plan.datedCitations.length > 0) {
|
|
314
|
+
logInfo(
|
|
315
|
+
`${plural(plan.datedCitations.length, 'file')} carry a dated citation, left alone:`,
|
|
316
|
+
)
|
|
317
|
+
for (const entry of plan.datedCitations) {
|
|
318
|
+
for (const line of entry.lines) {
|
|
319
|
+
logInfo(` ${entry.path}:${line.line} ${excerpt(line.text)}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
298
323
|
}
|
|
299
324
|
|
|
300
325
|
function toRecordsRecord(
|
|
@@ -313,7 +338,11 @@ function toRecordsRecord(
|
|
|
313
338
|
excluded: plan.excluded.length,
|
|
314
339
|
coupled: plan.coupled,
|
|
315
340
|
frontmatterGlobs: plan.frontmatterGlobs,
|
|
341
|
+
crossRepoCitations: plan.crossRepoCitations,
|
|
342
|
+
datedCitations: plan.datedCitations,
|
|
316
343
|
globs: plan.globs,
|
|
344
|
+
crossRepo: plan.crossRepo,
|
|
345
|
+
dated: plan.dated,
|
|
317
346
|
records,
|
|
318
347
|
paths: plan.entries.map((entry) => ({
|
|
319
348
|
path: entry.path,
|
|
@@ -575,6 +604,11 @@ export function register(program: Command): void {
|
|
|
575
604
|
'alone rather than rewritten, since a rewritten glob stops matching',
|
|
576
605
|
'silently. No marker is needed; the YAML shape is enough.',
|
|
577
606
|
'',
|
|
607
|
+
'A citation shaped like a path into another repository, or one sitting',
|
|
608
|
+
'inside a dated paragraph, is reported and left alone rather than',
|
|
609
|
+
'rewritten. The first is an ownership judgment and the second a tense',
|
|
610
|
+
'one, neither of which a regular expression makes reliably.',
|
|
611
|
+
'',
|
|
578
612
|
'The records themselves are never swept. Everything under .canon/ and',
|
|
579
613
|
'every .claude/ record folder is left alone and reported as a count, so',
|
|
580
614
|
'a run after the ignore entries collapse touches the same files as one',
|
package/src/migrate/records.ts
CHANGED
|
@@ -257,7 +257,60 @@ function isFrontmatterPathsLine(
|
|
|
257
257
|
return lines[top]?.trim().startsWith('paths:') ?? false
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
/**
|
|
261
|
+
* A path segment immediately before a citation match, shaped like another
|
|
262
|
+
* repository's own name: it carries a dot (a domain-shaped token, matching
|
|
263
|
+
* the measured `erclx.dev`) or it is itself preceded by a further `/` (two
|
|
264
|
+
* path segments deep, also matching the measured `public/erclx.dev/...`).
|
|
265
|
+
*
|
|
266
|
+
* Bounded by `[\w.-]`, which is what keeps a shell-glob prefix such as
|
|
267
|
+
* `*` before the slash and a variable-substitution prefix such as
|
|
268
|
+
* `$project` out: neither `*` nor `$` is in the class, so a segment built
|
|
269
|
+
* from either never reaches the dot or the second slash this looks for.
|
|
270
|
+
*/
|
|
271
|
+
const CROSS_REPO_PREFIX = /(?:[\w.-]+\.[\w.-]+|[\w.-]+\/[\w.-]+)\/$/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Whether the line's citation sits right after a prefix `CROSS_REPO_PREFIX`
|
|
275
|
+
* reads as another repository's own path. Per-line rather than per-match,
|
|
276
|
+
* matching `isKept`'s and `isFrontmatterPathsLine`'s granularity: a line
|
|
277
|
+
* carrying more than one citation is judged by the first.
|
|
278
|
+
*/
|
|
279
|
+
function isCrossRepoPrefix(lines: readonly string[], index: number): boolean {
|
|
280
|
+
const line = lines[index] ?? ''
|
|
281
|
+
const match = [...line.matchAll(CITATION)][0]
|
|
282
|
+
if (match === undefined) return false
|
|
283
|
+
|
|
284
|
+
return CROSS_REPO_PREFIX.test(line.slice(0, match.index ?? 0))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** An ISO date, marking a paragraph as recording what was true on that day. */
|
|
288
|
+
const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Whether the line's citation sits inside a blank-line-delimited paragraph
|
|
292
|
+
* that also carries an ISO date, read as dated prose rather than a live path.
|
|
293
|
+
*
|
|
294
|
+
* Walks both directions from `index` to the paragraph's edges, unlike
|
|
295
|
+
* `isKept`'s upward-only walk to a marker: the date can close a paragraph the
|
|
296
|
+
* citation opens, as `.claude/ARCHITECTURE.md`'s own `Measured at ... on
|
|
297
|
+
* <date>` sentences do.
|
|
298
|
+
*/
|
|
299
|
+
function isDatedParagraph(lines: readonly string[], index: number): boolean {
|
|
300
|
+
let start = index
|
|
301
|
+
while (start > 0 && lines[start - 1]?.trim() !== '') start -= 1
|
|
302
|
+
|
|
303
|
+
let end = index
|
|
304
|
+
while (end < lines.length - 1 && lines[end + 1]?.trim() !== '') end += 1
|
|
305
|
+
|
|
306
|
+
for (let cursor = start; cursor <= end; cursor += 1) {
|
|
307
|
+
if (ISO_DATE.test(lines[cursor] ?? '')) return true
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
type LineClass = 'live' | 'kept' | 'glob' | 'crossRepo' | 'dated'
|
|
261
314
|
|
|
262
315
|
function classifyLine(
|
|
263
316
|
lines: readonly string[],
|
|
@@ -266,6 +319,8 @@ function classifyLine(
|
|
|
266
319
|
): LineClass {
|
|
267
320
|
if (isFrontmatterPathsLine(lines, index, frontmatterEndIndex)) return 'glob'
|
|
268
321
|
if (isKept(lines, index)) return 'kept'
|
|
322
|
+
if (isCrossRepoPrefix(lines, index)) return 'crossRepo'
|
|
323
|
+
if (isDatedParagraph(lines, index)) return 'dated'
|
|
269
324
|
return 'live'
|
|
270
325
|
}
|
|
271
326
|
|
|
@@ -287,8 +342,9 @@ export function rewriteText(text: string): string {
|
|
|
287
342
|
|
|
288
343
|
/**
|
|
289
344
|
* How many citations `rewriteText` would rewrite, how many marked lines it
|
|
290
|
-
* left alone,
|
|
291
|
-
*
|
|
345
|
+
* left alone, how many sat inside a frontmatter `paths:` glob, how many sat
|
|
346
|
+
* behind a cross-repository-shaped prefix, and how many sat inside a dated
|
|
347
|
+
* paragraph. Every number past the first is what says its boundary fired at
|
|
292
348
|
* all, which a diff cannot show because a protected line does not appear in
|
|
293
349
|
* one.
|
|
294
350
|
*/
|
|
@@ -296,12 +352,16 @@ export function scanText(text: string): {
|
|
|
296
352
|
readonly rewritten: number
|
|
297
353
|
readonly kept: number
|
|
298
354
|
readonly globs: number
|
|
355
|
+
readonly crossRepo: number
|
|
356
|
+
readonly dated: number
|
|
299
357
|
} {
|
|
300
358
|
const lines = text.split('\n')
|
|
301
359
|
const frontmatterEndIndex = frontmatterEnd(lines)
|
|
302
360
|
let rewritten = 0
|
|
303
361
|
let kept = 0
|
|
304
362
|
let globs = 0
|
|
363
|
+
let crossRepo = 0
|
|
364
|
+
let dated = 0
|
|
305
365
|
|
|
306
366
|
for (const [index, line] of lines.entries()) {
|
|
307
367
|
const matches = [...line.matchAll(CITATION)].length
|
|
@@ -310,10 +370,12 @@ export function scanText(text: string): {
|
|
|
310
370
|
const kind = classifyLine(lines, index, frontmatterEndIndex)
|
|
311
371
|
if (kind === 'glob') globs += matches
|
|
312
372
|
else if (kind === 'kept') kept += matches
|
|
373
|
+
else if (kind === 'crossRepo') crossRepo += matches
|
|
374
|
+
else if (kind === 'dated') dated += matches
|
|
313
375
|
else rewritten += matches
|
|
314
376
|
}
|
|
315
377
|
|
|
316
|
-
return { rewritten, kept, globs }
|
|
378
|
+
return { rewritten, kept, globs, crossRepo, dated }
|
|
317
379
|
}
|
|
318
380
|
|
|
319
381
|
/** Where a citation sits, so a reader can judge it before `--write` runs. */
|
|
@@ -338,6 +400,45 @@ function frontmatterGlobLines(text: string): CitationLine[] {
|
|
|
338
400
|
return held
|
|
339
401
|
}
|
|
340
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Every line in `text` whose citation `classifyLine` reads as `crossRepo`.
|
|
405
|
+
*
|
|
406
|
+
* Reads through `classifyLine` rather than `isCrossRepoPrefix` alone, so a
|
|
407
|
+
* line a glob or a keep marker already claims is not reported twice under a
|
|
408
|
+
* second boundary.
|
|
409
|
+
*/
|
|
410
|
+
function crossRepoLines(text: string): CitationLine[] {
|
|
411
|
+
const lines = text.split('\n')
|
|
412
|
+
const frontmatterEndIndex = frontmatterEnd(lines)
|
|
413
|
+
const held: CitationLine[] = []
|
|
414
|
+
|
|
415
|
+
for (const [index, line] of lines.entries()) {
|
|
416
|
+
if ([...line.matchAll(CITATION)].length === 0) continue
|
|
417
|
+
if (classifyLine(lines, index, frontmatterEndIndex) !== 'crossRepo')
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
held.push({ line: index + 1, text: line.trim() })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return held
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Every line in `text` whose citation `classifyLine` reads as `dated`. */
|
|
427
|
+
function datedLines(text: string): CitationLine[] {
|
|
428
|
+
const lines = text.split('\n')
|
|
429
|
+
const frontmatterEndIndex = frontmatterEnd(lines)
|
|
430
|
+
const held: CitationLine[] = []
|
|
431
|
+
|
|
432
|
+
for (const [index, line] of lines.entries()) {
|
|
433
|
+
if ([...line.matchAll(CITATION)].length === 0) continue
|
|
434
|
+
if (classifyLine(lines, index, frontmatterEndIndex) !== 'dated') continue
|
|
435
|
+
|
|
436
|
+
held.push({ line: index + 1, text: line.trim() })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return held
|
|
440
|
+
}
|
|
441
|
+
|
|
341
442
|
export interface FolderMove {
|
|
342
443
|
readonly from: string
|
|
343
444
|
readonly to: string
|
|
@@ -402,6 +503,18 @@ export interface FrontmatterGlobEntry {
|
|
|
402
503
|
readonly lines: readonly CitationLine[]
|
|
403
504
|
}
|
|
404
505
|
|
|
506
|
+
/** One file whose citation resolves outside this project, held rather than rewritten. */
|
|
507
|
+
export interface CrossRepoCitationEntry {
|
|
508
|
+
readonly path: string
|
|
509
|
+
readonly lines: readonly CitationLine[]
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** One file whose citation sits inside a dated paragraph, held rather than rewritten. */
|
|
513
|
+
export interface DatedCitationEntry {
|
|
514
|
+
readonly path: string
|
|
515
|
+
readonly lines: readonly CitationLine[]
|
|
516
|
+
}
|
|
517
|
+
|
|
405
518
|
export interface RecordsPlan {
|
|
406
519
|
readonly moves: readonly FolderMove[]
|
|
407
520
|
readonly collisions: readonly string[]
|
|
@@ -409,9 +522,13 @@ export interface RecordsPlan {
|
|
|
409
522
|
readonly excluded: readonly string[]
|
|
410
523
|
readonly coupled: readonly string[]
|
|
411
524
|
readonly frontmatterGlobs: readonly FrontmatterGlobEntry[]
|
|
525
|
+
readonly crossRepoCitations: readonly CrossRepoCitationEntry[]
|
|
526
|
+
readonly datedCitations: readonly DatedCitationEntry[]
|
|
412
527
|
readonly rewritten: number
|
|
413
528
|
readonly kept: number
|
|
414
529
|
readonly globs: number
|
|
530
|
+
readonly crossRepo: number
|
|
531
|
+
readonly dated: number
|
|
415
532
|
}
|
|
416
533
|
|
|
417
534
|
/**
|
|
@@ -431,8 +548,12 @@ export function planRecordsMove(
|
|
|
431
548
|
const excluded: string[] = []
|
|
432
549
|
const coupled: string[] = []
|
|
433
550
|
const frontmatterGlobs: FrontmatterGlobEntry[] = []
|
|
551
|
+
const crossRepoCitations: CrossRepoCitationEntry[] = []
|
|
552
|
+
const datedCitations: DatedCitationEntry[] = []
|
|
434
553
|
let kept = 0
|
|
435
554
|
let globs = 0
|
|
555
|
+
let crossRepo = 0
|
|
556
|
+
let dated = 0
|
|
436
557
|
|
|
437
558
|
for (const source of sources) {
|
|
438
559
|
// Silently, and ahead of the exclusion test. The command boundary filters
|
|
@@ -452,13 +573,24 @@ export function planRecordsMove(
|
|
|
452
573
|
const counts = scanText(source.text)
|
|
453
574
|
kept += counts.kept
|
|
454
575
|
globs += counts.globs
|
|
576
|
+
crossRepo += counts.crossRepo
|
|
577
|
+
dated += counts.dated
|
|
455
578
|
|
|
456
579
|
// Ahead of the rewritten === 0 continue below: a file whose only citation
|
|
457
|
-
// sits in its paths: glob
|
|
580
|
+
// sits in its paths: glob, a cross-repo prefix, or a dated paragraph still
|
|
581
|
+
// needs to be reported.
|
|
458
582
|
const held = frontmatterGlobLines(source.text)
|
|
459
583
|
if (held.length > 0)
|
|
460
584
|
frontmatterGlobs.push({ path: source.path, lines: held })
|
|
461
585
|
|
|
586
|
+
const foreign = crossRepoLines(source.text)
|
|
587
|
+
if (foreign.length > 0)
|
|
588
|
+
crossRepoCitations.push({ path: source.path, lines: foreign })
|
|
589
|
+
|
|
590
|
+
const dates = datedLines(source.text)
|
|
591
|
+
if (dates.length > 0)
|
|
592
|
+
datedCitations.push({ path: source.path, lines: dates })
|
|
593
|
+
|
|
462
594
|
if (counts.rewritten === 0) continue
|
|
463
595
|
|
|
464
596
|
if (referencesExcluded(source.text)) coupled.push(source.path)
|
|
@@ -478,8 +610,12 @@ export function planRecordsMove(
|
|
|
478
610
|
excluded,
|
|
479
611
|
coupled,
|
|
480
612
|
frontmatterGlobs,
|
|
613
|
+
crossRepoCitations,
|
|
614
|
+
datedCitations,
|
|
481
615
|
rewritten: entries.reduce((sum, entry) => sum + entry.rewritten, 0),
|
|
482
616
|
kept,
|
|
483
617
|
globs,
|
|
618
|
+
crossRepo,
|
|
619
|
+
dated,
|
|
484
620
|
}
|
|
485
621
|
}
|
package/standards/versioning.md
CHANGED
|
@@ -18,6 +18,7 @@ Does not govern:
|
|
|
18
18
|
- Commit subject, branch name, and pull request title format: `commit.md`, `branch.md`, and `pr.md`
|
|
19
19
|
- Voice, rhythm, and sentence construction in any text carrying a label: the `write-human` skill
|
|
20
20
|
- Punctuation, formatting, and word choice in any text carrying a label: `markdown.md`
|
|
21
|
+
- How a quoted-alone version-shaped token or a record path is caught as a board reference, which sits outside the phase-label/semver split this file owns: `publish.md`
|
|
21
22
|
|
|
22
23
|
## Phase labels
|
|
23
24
|
|
|
@@ -59,6 +60,10 @@ External release identity used in git tags and release notes. Independent of pha
|
|
|
59
60
|
- Git tags use semver only. Phase labels never become tags.
|
|
60
61
|
- A PR that cuts a release may reference its semver tag in the title or body. Phase labels still do not appear.
|
|
61
62
|
- PR bodies, review comments, and issue text name the change itself, never the internal stream that scheduled it. Describe the work rather than the label it was planned under.
|
|
63
|
+
- A phase label written alone inside its own code span reads as shown rather than asserted. `canon labels scan` masks a code span before checking for a bare token, so a backticked label on its own does not count as an appearance for the phase-label check.
|
|
64
|
+
- That masking clears one check and not the gate. A span whose whole content is the label still resolves as a board reference under `publish.md`'s own board-identifier check, run by the same scan, so a title or body quoting a label alone in a span still fails on that check and does not clear every surface the table above marks `no`.
|
|
65
|
+
- The one shape neither check reaches is a version-shaped token folded into a longer quoted phrase, such as a fixture name or a file path. A backticked span quoting a test fixture's own version-shaped name is the corpus case that forced this carve-out, and a real phase label folded the same way inside a review comment is the counterexample where it read as clean on both checks.
|
|
66
|
+
- The operator decided to keep the carve-out rather than narrow it to fenced blocks alone or drop it outright, leaving an author responsible for writing a live reference in plain text instead of folding it into a quoted phrase.
|
|
62
67
|
- A phase label in a context entry is permitted only when its substance is restated inline beside it. A bare label carrying nothing beside it is forbidden, since a reader without the board has nothing to resolve it against.
|
|
63
68
|
- A phase label carries no exception for a surface this repository authors and ships to a target it does not control: a standard, a skill body, and a governance rule are all forbidden outright, substance restated or not. The line is ownership rather than reachability. A shipped file is read by a project that never edits it, so the label names a board that reader can never reach. A context entry stays permitted above because it is this project's own tracked surface, never shipped.
|
|
64
69
|
- The shipped-surface rule governs a reference to a real row, never a token shown to illustrate the label's own format. A phase label naming no decision, written only to show the pattern's shape, is not a citation, which is what lets a standard defining the format still show what one looks like.
|