@homeflare/config 0.12.0 → 0.13.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/dist/repo-shape/ci.d.ts.map +1 -1
- package/dist/repo-shape/drift.d.ts +1 -1
- package/dist/repo-shape/drift.d.ts.map +1 -1
- package/dist/repo-shape/guards.d.ts +5 -0
- package/dist/repo-shape/guards.d.ts.map +1 -1
- package/dist/repo-shape/refresh.d.ts +5 -1
- package/dist/repo-shape/refresh.d.ts.map +1 -1
- package/dist/repo-shape/retired.d.ts +54 -0
- package/dist/repo-shape/retired.d.ts.map +1 -0
- package/dist/repo-shape.d.ts +1 -0
- package/dist/repo-shape.d.ts.map +1 -1
- package/dist/repo-shape.js +84 -5
- package/dist/repo-shape.js.map +8 -7
- package/dist/versions.js +22 -1
- package/dist/versions.js.map +4 -4
- package/docs/repo-shape-retired.md +75 -0
- package/docs/repo-shape.md +11 -0
- package/package.json +1 -1
- package/src/repo-shape/ci.ts +15 -0
- package/src/repo-shape/drift.ts +19 -1
- package/src/repo-shape/guards.ts +11 -0
- package/src/repo-shape/refresh.ts +55 -2
- package/src/repo-shape/retired.ts +113 -0
- package/src/repo-shape.ts +8 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Retired rendered files — the fossil, and how it is cleared
|
|
2
|
+
|
|
3
|
+
`renderRepoShape` only emits the files a shape asks for today. `refreshRepoShape` only
|
|
4
|
+
writes what it renders. Put those two together and a file the renderer stops emitting is
|
|
5
|
+
never deleted by a refresh — it just stops being updated, silently, in every repository
|
|
6
|
+
that had already taken it. `RETIRED_FILES` in `src/repo-shape/retired.ts` is how that stops
|
|
7
|
+
being permanent.
|
|
8
|
+
|
|
9
|
+
## MEASURED 2026-09-24: the gap that motivated this
|
|
10
|
+
|
|
11
|
+
`@homeflare/config` 0.12.0 ([kit PR 199](https://github.com/taslabs-net/homeflare-kit/pull/199))
|
|
12
|
+
stopped rendering `.github/workflows/dependabot-automerge.yml` — `taslabs-net/homeflare-bumper`
|
|
13
|
+
carries a kit release into a consumer now, over `workflow_dispatch`, so the workflow that used
|
|
14
|
+
to arm auto-merge on Dependabot's `homeflare` group had nothing left to do.
|
|
15
|
+
|
|
16
|
+
Every repository that had already refreshed to 0.12.0 before this module existed
|
|
17
|
+
(`homeflare-wiki` bump PR 19, `homeflare-mini` bump PR 47) kept the dead file. Nothing said
|
|
18
|
+
so: `driftInRepoShape` only ever compared the paths the **current** shape renders, and a
|
|
19
|
+
retired path is not one of those, so it was invisible to the one check whose whole job is
|
|
20
|
+
to say when a committed file and the renderer disagree.
|
|
21
|
+
|
|
22
|
+
## The two halves
|
|
23
|
+
|
|
24
|
+
**`driftInRepoShape` (`repo-shape check`) reports a retired path that is merely present.**
|
|
25
|
+
It does not read the file to decide anything beyond "is it there" — proving the file is
|
|
26
|
+
provably ours is `refreshRepoShape`'s decision to make, not the read-only check's. So
|
|
27
|
+
`bun run repo-shape:refresh` fails on a retired fossil exactly the way it fails on ordinary
|
|
28
|
+
drift:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
✗ .github/workflows/dependabot-automerge.yml: retired in @homeflare/config@0.12.0
|
|
32
|
+
(kit PR 199 retired the Dependabot @homeflare group; taslabs-net/homeflare-bumper carries
|
|
33
|
+
a kit release into each consumer now, over the kit's own release workflow) but still
|
|
34
|
+
present — run `bun run repo-shape:refresh` to remove it
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**`refreshRepoShape` deletes a retired path only when it can prove it rendered it.** The
|
|
38
|
+
proof is the generated-file header: every renderer in `src/repo-shape/` writes
|
|
39
|
+
`🤖 RENDERED BY @homeflare/config` into the file it emits, and `wasRenderedByUs` is nothing
|
|
40
|
+
more than a check for that line. A retired path whose content carries it is deleted. A
|
|
41
|
+
retired path whose content does not — a hand-written replacement, a fork of the old
|
|
42
|
+
rendered file kept on purpose, or coincidental content that landed at the same name — is
|
|
43
|
+
left exactly as it is, and reported back as `refused`, never `removed`.
|
|
44
|
+
|
|
45
|
+
⚠️ **A refused file still fails `repo-shape check`.** The check does not know or care
|
|
46
|
+
whether the file is provably ours; it only knows the retired path is present. There is no
|
|
47
|
+
`except()` for a retired path — a repository that genuinely wants to keep a hand-written
|
|
48
|
+
file at that exact name has to remove it or rename it itself; `repo-shape` will not delete
|
|
49
|
+
someone else's file to make its own check pass.
|
|
50
|
+
|
|
51
|
+
## Why the header, not a stored copy of the old rendered text
|
|
52
|
+
|
|
53
|
+
The retired `dependabot-automerge.yml` varied by `shape.runner` and by repository name —
|
|
54
|
+
there is no single byte-for-byte "the rendered form" to compare against across the estate's
|
|
55
|
+
history, and a renderer that has been deleted cannot be asked to re-render its old output.
|
|
56
|
+
The header line is the one thing every variant, in every repository, always carried.
|
|
57
|
+
|
|
58
|
+
## Retiring a path, going forward
|
|
59
|
+
|
|
60
|
+
Add an entry to `RETIRED_FILES` in `src/repo-shape/retired.ts`: the path, the
|
|
61
|
+
`@homeflare/config` version whose release stops rendering it, and a written reason — same
|
|
62
|
+
rule as `except()`, enforced the same way. Remove the path from `render.ts` in the same
|
|
63
|
+
changeset. A path can never move back into `RenderedPath`; `repo-shape-retired.test.ts`
|
|
64
|
+
asserts the two lists stay disjoint, because a path in both would have the renderer writing
|
|
65
|
+
a file this module is also trying to delete, every single refresh.
|
|
66
|
+
|
|
67
|
+
## How this reaches a consumer
|
|
68
|
+
|
|
69
|
+
A patch release of `@homeflare/config` ships the new `RETIRED_FILES` entry.
|
|
70
|
+
`taslabs-net/homeflare-bumper` opens the bump pull request in each consumer the way it
|
|
71
|
+
always does. Its own `bun run repo-shape:refresh` — the same step every bump PR already
|
|
72
|
+
needs, [documented here](./repo-shape.md#bumping-homeflareconfig-will-go-red-before-it-goes-green) — now also deletes
|
|
73
|
+
the fossil, so the PR's diff touches `.github/workflows/`. **The bumper holds a PR that
|
|
74
|
+
touches that directory for Tim rather than auto-merging it**, which is the correct,
|
|
75
|
+
existing behavior for a workflow-file change — not something this module has to arrange.
|
package/docs/repo-shape.md
CHANGED
|
@@ -173,6 +173,17 @@ The alternative — a check that tolerated an older render — is a check that t
|
|
|
173
173
|
drift, which is the thing this exists to stop. A loud, one-command failure is the better
|
|
174
174
|
half of that trade, but it is a trade.
|
|
175
175
|
|
|
176
|
+
## A file the renderer stops emitting is deleted, not left behind
|
|
177
|
+
|
|
178
|
+
`renderRepoShape` only emits what a shape asks for today, and `refreshRepoShape` only
|
|
179
|
+
writes what it renders — so on its own, a file the renderer retires would never get
|
|
180
|
+
deleted by a refresh; it would just stop being updated, in every repository that already
|
|
181
|
+
had it. `RETIRED_FILES` in `src/repo-shape/retired.ts` closes that gap: `repo-shape check`
|
|
182
|
+
flags a retired path that is still present, and a refresh deletes it — but only when the
|
|
183
|
+
file provably carries this package's generated-file header, never a hand-written file that
|
|
184
|
+
happens to share the name. [repo-shape-retired.md](repo-shape-retired.md) has the mechanism
|
|
185
|
+
and the measurement that found the first fossil.
|
|
186
|
+
|
|
176
187
|
## What this does not render yet
|
|
177
188
|
|
|
178
189
|
`.github/workflows/release.yml`. Thirteen copies, 166–197 lines each, thirteen distinct
|
package/package.json
CHANGED
package/src/repo-shape/ci.ts
CHANGED
|
@@ -74,6 +74,21 @@ permissions:
|
|
|
74
74
|
concurrency:
|
|
75
75
|
group: ci-\${{ github.ref }}
|
|
76
76
|
cancel-in-progress: true
|
|
77
|
+
|
|
78
|
+
# ⛔ SECOND GUARD, NOT THE FIRST ONE. The estate's primary Alchemy telemetry opt-out is the
|
|
79
|
+
# persisted \`~/.alchemy/telemetry-disabled\` file (Tim, 2026-09-26), which a CI runner never
|
|
80
|
+
# has — a self-hosted job starts a fresh container per run and a GitHub-hosted one is a fresh
|
|
81
|
+
# VM every time, so neither has read the file even if this estate's account somehow wrote it
|
|
82
|
+
# there. \`DO_NOT_TRACK\` is the opt-out alchemy's telemetry layer actually reads
|
|
83
|
+
# (\`!!process.env.DO_NOT_TRACK\`, alchemy \`src/Telemetry/Attributes.ts:126-129\`); this env
|
|
84
|
+
# applies to every job and step in this workflow. No repository's \`check\` invokes the Alchemy
|
|
85
|
+
# CLI today, measured 2026-09-26 (grepped every ci/security/release workflow for
|
|
86
|
+
# \`alchemy\`/\`bin/cli.js\`, zero hits), but alchemy's own \`Test\` helpers wire \`TelemetryLive\`
|
|
87
|
+
# into top-level \`deploy\`/\`destroy\` (\`src/Test/Core.ts:470,482\`), so a future kit test built on
|
|
88
|
+
# those (today's kit tests use only \`test.provider\` scratch stacks, which don't) would send
|
|
89
|
+
# telemetry from \`bun test\` without this guard — this closes that gap in advance.
|
|
90
|
+
env:
|
|
91
|
+
DO_NOT_TRACK: '1'
|
|
77
92
|
`;
|
|
78
93
|
|
|
79
94
|
const CHECK_NOTE = ` # ── One job, one install ────────────────────────────────────────────────────
|
package/src/repo-shape/drift.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* The one exception is line endings, normalised so a CRLF checkout is not "drift".
|
|
19
19
|
*/
|
|
20
20
|
import { type RenderedRepo, renderRepoShape } from './render.ts';
|
|
21
|
+
import { RETIRED_FILES, retiredFileProblem } from './retired.ts';
|
|
21
22
|
import type { RenderedPath, RepoShape, RepoShapeException } from './shape.ts';
|
|
22
23
|
|
|
23
24
|
/** One thing to fix, in the imperative — the same `Problem` shape `./check` reports. */
|
|
@@ -69,8 +70,24 @@ function duplicateExceptions(exceptions: readonly RepoShapeException[]): Problem
|
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* ★ A RETIRED PATH IS REPORTED WHETHER OR NOT IT IS PROVABLY OURS. This check only reads
|
|
75
|
+
* the file to see if it is THERE — `wasRenderedByUs` is `refreshRepoShape`'s call to
|
|
76
|
+
* make, because only the writer should decide whether to delete. Reporting here is what
|
|
77
|
+
* makes a fossil visible even in a repository that only ever runs `--check` in CI.
|
|
78
|
+
*/
|
|
79
|
+
async function retiredFilesPresent(projectDir: string): Promise<Problem[]> {
|
|
80
|
+
const problems: Problem[] = [];
|
|
81
|
+
for (const file of RETIRED_FILES) {
|
|
82
|
+
if ((await readIfPresent(`${projectDir}/${file.path}`)) !== undefined) {
|
|
83
|
+
problems.push(retiredFileProblem(file));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return problems;
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
export interface DriftReport {
|
|
73
|
-
/** Empty when the committed files are the rendered ones. */
|
|
90
|
+
/** Empty when the committed files are the rendered ones and no retired path lingers. */
|
|
74
91
|
readonly problems: readonly Problem[];
|
|
75
92
|
/** Paths whose committed text differs from the render, excluding excepted files. */
|
|
76
93
|
readonly drifted: readonly string[];
|
|
@@ -93,6 +110,7 @@ export async function driftInRepoShape(projectDir: string, shape: RepoShape): Pr
|
|
|
93
110
|
const problems: Problem[] = [
|
|
94
111
|
...duplicateExceptions(exceptions),
|
|
95
112
|
...staleExceptions(rendered, exceptions),
|
|
113
|
+
...(await retiredFilesPresent(projectDir)),
|
|
96
114
|
];
|
|
97
115
|
const drifted: string[] = [];
|
|
98
116
|
|
package/src/repo-shape/guards.ts
CHANGED
|
@@ -36,6 +36,17 @@ export function requireIsoDate(value: string): string {
|
|
|
36
36
|
return value;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A retired file names the `@homeflare/config` release that stopped rendering it, so
|
|
41
|
+
* anyone reading `retired.ts` can find the changeset that made the call.
|
|
42
|
+
*/
|
|
43
|
+
export function requireSemver(value: string): string {
|
|
44
|
+
if (!/^\d+\.\d+\.\d+$/.test(value)) {
|
|
45
|
+
throw new Error(`repo-shape: retiredIn must be a released x.y.z, got ${JSON.stringify(value)}`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/**
|
|
40
51
|
* ⛔ A MAJOR, NOT A RANGE, AND NOT A FLOAT. `actions/setup-node` takes `node-version: 24`
|
|
41
52
|
* and resolves the newest 24.x; a fractional or negative value renders YAML the action
|
|
@@ -12,9 +12,17 @@
|
|
|
12
12
|
* ⚠️ IT NEVER WRITES AN EXCEPTED FILE. Refreshing a file the repository declared it owns
|
|
13
13
|
* would overwrite the deviation the reason was written for — the one destructive thing
|
|
14
14
|
* this could do, and the one it must not.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ IT DELETES A RETIRED FILE ONLY WHEN IT CAN PROVE IT RENDERED IT. `RETIRED_FILES`
|
|
17
|
+
* (`retired.ts`) names every path this package used to render; `wasRenderedByUs` is the
|
|
18
|
+
* proof — the file still carries the generated-file header. A retired path present
|
|
19
|
+
* without that header is left alone and reported as refused, on the same reasoning as
|
|
20
|
+
* never overwriting an excepted file: this writer only ever removes what it is sure is
|
|
21
|
+
* its own.
|
|
15
22
|
*/
|
|
16
23
|
import { REFRESH_COMMAND, driftInRepoShape, exceptionSummary } from './drift.ts';
|
|
17
24
|
import { renderRepoShape } from './render.ts';
|
|
25
|
+
import { RETIRED_FILES, wasRenderedByUs } from './retired.ts';
|
|
18
26
|
import { type RepoShape, isExcepted } from './shape.ts';
|
|
19
27
|
import type { RenderedPath } from './shape.ts';
|
|
20
28
|
|
|
@@ -25,9 +33,13 @@ export interface RefreshResult {
|
|
|
25
33
|
readonly unchanged: readonly string[];
|
|
26
34
|
/** Paths skipped because the shape declares an exception for them. */
|
|
27
35
|
readonly skipped: readonly string[];
|
|
36
|
+
/** Retired paths deleted because they still carried the generated-file header. */
|
|
37
|
+
readonly removed: readonly string[];
|
|
38
|
+
/** Retired paths left alone because they did not — provably hand-written, not ours. */
|
|
39
|
+
readonly refused: readonly string[];
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
/** Write a repository's rendered files into `projectDir
|
|
42
|
+
/** Write a repository's rendered files into `projectDir`, and clear its rendered fossils. */
|
|
31
43
|
export async function refreshRepoShape(
|
|
32
44
|
projectDir: string,
|
|
33
45
|
shape: RepoShape,
|
|
@@ -52,7 +64,36 @@ export async function refreshRepoShape(
|
|
|
52
64
|
written.push(path);
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
const { refused, removed } = await clearRetiredFiles(projectDir);
|
|
68
|
+
|
|
69
|
+
return { refused, removed, skipped, unchanged, written };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete every retired path this package can prove it rendered; leave every other one, and
|
|
74
|
+
* say so. A path absent from `projectDir` is neither removed nor refused — there is
|
|
75
|
+
* nothing there to have an opinion about.
|
|
76
|
+
*/
|
|
77
|
+
async function clearRetiredFiles(
|
|
78
|
+
projectDir: string,
|
|
79
|
+
): Promise<{ removed: string[]; refused: string[] }> {
|
|
80
|
+
const removed: string[] = [];
|
|
81
|
+
const refused: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const retiredFile of RETIRED_FILES) {
|
|
84
|
+
const target = `${projectDir}/${retiredFile.path}`;
|
|
85
|
+
const file = Bun.file(target);
|
|
86
|
+
if (!(await file.exists())) continue;
|
|
87
|
+
|
|
88
|
+
if (wasRenderedByUs(await file.text())) {
|
|
89
|
+
await file.delete();
|
|
90
|
+
removed.push(retiredFile.path);
|
|
91
|
+
} else {
|
|
92
|
+
refused.push(retiredFile.path);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { refused, removed };
|
|
56
97
|
}
|
|
57
98
|
|
|
58
99
|
/**
|
|
@@ -108,6 +149,18 @@ export async function repoShapeCli(
|
|
|
108
149
|
...result.written.map((path) => `wrote ${path}`),
|
|
109
150
|
...result.unchanged.map((path) => `ok ${path}`),
|
|
110
151
|
...result.skipped.map((path) => `excepted ${path}`),
|
|
152
|
+
...result.removed.map((path) => `removed ${path} (retired; carried our header)`),
|
|
111
153
|
]);
|
|
154
|
+
// ★ ON ITS OWN LINE, TO STDERR: a refused retired file is the one outcome here that
|
|
155
|
+
// still needs a person. `--check` (above) keeps failing on it — it reports any retired
|
|
156
|
+
// path that is present, proof or not — so this is not the only place it is said, but
|
|
157
|
+
// it is the only place that says WHY refresh did not just fix it.
|
|
158
|
+
await say(
|
|
159
|
+
Bun.stderr,
|
|
160
|
+
result.refused.map(
|
|
161
|
+
(path) =>
|
|
162
|
+
`refused ${path} — present but not provably ours; not deleted. See docs/repo-shape-retired.md.`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
112
165
|
return 0;
|
|
113
166
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendered paths this package stopped rendering — the "no fossils" list.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY THIS EXISTS. `renderRepoShape` only emits the files a shape asks for today; it
|
|
5
|
+
* never says what it used to emit, and `refreshRepoShape` only writes what it renders —
|
|
6
|
+
* so a file retired from the renderer was never deleted by a refresh. It just stopped
|
|
7
|
+
* being updated, silently, forever, in every repository that had already taken it.
|
|
8
|
+
*
|
|
9
|
+
* MEASURED 2026-09-24: `@homeflare/config` 0.12.0 (kit PR 199) stopped rendering
|
|
10
|
+
* `.github/workflows/dependabot-automerge.yml`. Every consumer that had refreshed to
|
|
11
|
+
* 0.12.0 before this module existed (homeflare-wiki bump PR 19, homeflare-mini bump
|
|
12
|
+
* PR 47) kept the dead file, and `driftInRepoShape` never said so — it only compares
|
|
13
|
+
* paths the CURRENT shape renders, and a retired path is not one of those.
|
|
14
|
+
*
|
|
15
|
+
* ⛔ RETIRING A PATH IS A ONE-WAY DOOR, NOT A RENAME. Once a path is here it can never
|
|
16
|
+
* become a `RenderedPath` again: `render.ts` would start writing a file this module is
|
|
17
|
+
* also trying to delete, and every refresh would fight itself. Give the replacement a
|
|
18
|
+
* new path instead. `repo-shape-retired.test.ts` asserts the two lists never overlap.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ DELETION IS GATED ON PROOF, NOT ON THE PATH ALONE. A repository can have a
|
|
21
|
+
* hand-written file sitting at the exact path a retired renderer used to own — a fork of
|
|
22
|
+
* the old rendered file, kept on purpose, or unrelated content that just landed there.
|
|
23
|
+
* `wasRenderedByUs` is the proof: every renderer in this directory writes the
|
|
24
|
+
* `🤖 RENDERED BY @homeflare/config` line into its header (grep the directory for it),
|
|
25
|
+
* and that line is the one thing a hand-written file has no reason to contain.
|
|
26
|
+
* `refreshRepoShape` deletes a retired path only when the line is present; otherwise it
|
|
27
|
+
* refuses and reports the path, the same way it refuses to overwrite an excepted file.
|
|
28
|
+
*/
|
|
29
|
+
import { requireSemver, requireSentence } from './guards.ts';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A path `@homeflare/config` no longer renders, but may still find committed in a
|
|
33
|
+
* repository that refreshed before the retirement. Extend this union at the same time a
|
|
34
|
+
* path is added to `RETIRED_FILES` below — never reuse one already there.
|
|
35
|
+
*/
|
|
36
|
+
export type RetiredPath = '.github/workflows/dependabot-automerge.yml';
|
|
37
|
+
|
|
38
|
+
export interface RetiredFile {
|
|
39
|
+
/** The path this package rendered, once. */
|
|
40
|
+
readonly path: RetiredPath;
|
|
41
|
+
/** The `@homeflare/config` version whose release stopped rendering it. */
|
|
42
|
+
readonly retiredIn: string;
|
|
43
|
+
/** Why, and which kit change did it — written at the retirement, like `except()`'s reason. */
|
|
44
|
+
readonly reason: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function retire(file: RetiredFile): RetiredFile {
|
|
48
|
+
return {
|
|
49
|
+
path: file.path,
|
|
50
|
+
reason: requireSentence('retired reason', file.reason),
|
|
51
|
+
retiredIn: requireSemver(file.retiredIn),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Every path `@homeflare/config` used to render, oldest retirement first. */
|
|
56
|
+
export const RETIRED_FILES: readonly RetiredFile[] = [
|
|
57
|
+
retire({
|
|
58
|
+
path: '.github/workflows/dependabot-automerge.yml',
|
|
59
|
+
reason:
|
|
60
|
+
'kit PR 199 retired the Dependabot @homeflare group; taslabs-net/homeflare-bumper ' +
|
|
61
|
+
"carries a kit release into each consumer now, over the kit's own release workflow",
|
|
62
|
+
retiredIn: '0.12.0',
|
|
63
|
+
}),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The substring every generated file's header has carried since this package's first
|
|
68
|
+
* renderer. Kept independent of the four current renderers' own copies of this text
|
|
69
|
+
* (`ci.ts`, `security.ts`, `dependabot.ts`, `companions.ts`) on purpose: a future wording
|
|
70
|
+
* change to the live header must not stop this module recognising a file rendered under
|
|
71
|
+
* the old one.
|
|
72
|
+
*/
|
|
73
|
+
export const GENERATED_FILE_MARKER = '🤖 RENDERED BY @homeflare/config';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether `content` is provably a file this package once wrote.
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ THE MARKER HAS TO START A LINE OF ITS OWN, NOT MERELY OCCUR SOMEWHERE IN THE TEXT.
|
|
79
|
+
* Every real header carries it as a whole comment line — `# 🤖 RENDERED BY
|
|
80
|
+
* @homeflare/config — DO NOT EDIT…`, third line in every renderer in this directory, so
|
|
81
|
+
* this does not require it to be the FIRST line — but a bare `content.includes(...)`
|
|
82
|
+
* would also match a hand-written file that merely *talks about* the marker, e.g. a
|
|
83
|
+
* comment reading "this file used to be 🤖 RENDERED BY @homeflare/config before it was
|
|
84
|
+
* retired; keeping it by hand now" — adversarial review, 2026-09-24, caught this exact
|
|
85
|
+
* case before it shipped. Requiring the marker at the start of a trimmed line is what a
|
|
86
|
+
* sentence built around it, rather than a header line consisting of it, cannot satisfy.
|
|
87
|
+
* `repo-shape-retired.test.ts` asserts the mid-sentence form stays refused.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ STILL A PREFIX CHECK ON THAT LINE, NOT A FULL-LINE EXACT MATCH. The retired renderer
|
|
90
|
+
* varied its trailing header prose by `shape.runner` and by repository name, and a
|
|
91
|
+
* future wording change to what follows the marker on a live renderer's header must not
|
|
92
|
+
* stop this recognising a file rendered under the old wording — only the marker itself,
|
|
93
|
+
* `🤖 RENDERED BY @homeflare/config`, has been constant across every renderer this
|
|
94
|
+
* package has ever shipped.
|
|
95
|
+
*/
|
|
96
|
+
export function wasRenderedByUs(content: string): boolean {
|
|
97
|
+
return content
|
|
98
|
+
.split('\n')
|
|
99
|
+
.some((line) => line.trimStart().startsWith(`# ${GENERATED_FILE_MARKER}`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The message `driftInRepoShape` reports for a retired path that is still present, and
|
|
104
|
+
* `repoShapeCli --check` prints verbatim. Names the fix command either way — whether that
|
|
105
|
+
* command can actually remove the file depends on `wasRenderedByUs`, which only
|
|
106
|
+
* `refreshRepoShape` (holding the file's contents) can decide.
|
|
107
|
+
*/
|
|
108
|
+
export function retiredFileProblem(file: RetiredFile): string {
|
|
109
|
+
return (
|
|
110
|
+
`${file.path}: retired in @homeflare/config@${file.retiredIn} (${file.reason}) ` +
|
|
111
|
+
'but still present — run `bun run repo-shape:refresh` to remove it'
|
|
112
|
+
);
|
|
113
|
+
}
|
package/src/repo-shape.ts
CHANGED
|
@@ -54,6 +54,14 @@ export {
|
|
|
54
54
|
RENDERED_PATHS,
|
|
55
55
|
renderRepoShape,
|
|
56
56
|
} from './repo-shape/render.ts';
|
|
57
|
+
export {
|
|
58
|
+
type RetiredFile,
|
|
59
|
+
type RetiredPath,
|
|
60
|
+
GENERATED_FILE_MARKER,
|
|
61
|
+
RETIRED_FILES,
|
|
62
|
+
retiredFileProblem,
|
|
63
|
+
wasRenderedByUs,
|
|
64
|
+
} from './repo-shape/retired.ts';
|
|
57
65
|
export { renderSecurity } from './repo-shape/security.ts';
|
|
58
66
|
export {
|
|
59
67
|
type ExtraJob,
|