@afokapu/atdd-bun 0.7.0 → 0.7.2
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/README.md +11 -3
- package/integrity.json +5 -5
- package/package.json +1 -1
- package/src/integrity.ts +33 -9
- package/src/setup.ts +17 -1
- package/templates/github/atdd-bun.yml +5 -0
package/README.md
CHANGED
|
@@ -77,8 +77,15 @@ profiles: [traceability, planner]
|
|
|
77
77
|
|
|
78
78
|
`all`, the hooks and the generated CI then run only those profiles. Any profile can still be run
|
|
79
79
|
by name (`bun run atdd-bun coder`) to see what remains. An unknown name or an empty list is an
|
|
80
|
-
error, never a silent run of nothing.
|
|
81
|
-
|
|
80
|
+
error, never a silent run of nothing.
|
|
81
|
+
|
|
82
|
+
With no `profiles:` field, every profile runs, but none is governed yet. The first explicit list is
|
|
83
|
+
the adoption that establishes the governed set, so a brownfield repository can declare
|
|
84
|
+
`profiles: [docs]` in an ordinary pull request. From then on, the integrity check reports, against
|
|
85
|
+
the base branch, removing a profile from the list and removing the list itself. The second closes
|
|
86
|
+
the two-step bypass `[docs, security]` → no list → `[docs]`. `init` writes a new `atdd-bun.yaml`
|
|
87
|
+
with every profile listed, so a greenfield repository is governed from its first commit; trim the
|
|
88
|
+
list before that commit to adopt gradually.
|
|
82
89
|
|
|
83
90
|
## Configuration
|
|
84
91
|
|
|
@@ -116,7 +123,8 @@ modify the toolkit itself, only the configuration it offers, and enable capabili
|
|
|
116
123
|
- the installed package differs from its published hashes;
|
|
117
124
|
- the dependency is not an npm registry version;
|
|
118
125
|
- a generated file (workflow, skills, instruction block, integrity test) was edited;
|
|
119
|
-
- `atdd-bun.yaml` is looser than on the base branch
|
|
126
|
+
- `atdd-bun.yaml` is looser than on the base branch (after the first explicit `profiles:` list,
|
|
127
|
+
dropping a profile or the list counts).
|
|
120
128
|
|
|
121
129
|
Each finding names its restore command.
|
|
122
130
|
|
package/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.2",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "4e612f6344fab29c38269ce0303b73c32f75911dfef16a3acefe382f068329a3",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/atdd-bun.planner/atdd-bun.planner.acceptance-identity.convention.yaml": "81c5c773d5ee15e8d99a2c237aa9846b110b2533df45ebf407d0e3ec6ed3dd03",
|
|
9
9
|
"conventions/atdd-bun.planner/atdd-bun.planner.identity-required.convention.yaml": "e96d7c1455d0c221072d82e7f2b55da1c4f9ceb17718eb6ed96affdeef37675b",
|
|
@@ -1175,20 +1175,20 @@
|
|
|
1175
1175
|
"src/enforce.ts": "d7232331c9f0fdd150f2ca4bbf1bd2e877de111c684d46b690fd7acdae59f39c",
|
|
1176
1176
|
"src/hooks.ts": "adbb72f7a83f53d596fd217f01f4ce32c5a59a23ab81993fed226fb96fd1cbd1",
|
|
1177
1177
|
"src/index.ts": "b3cc97f9867bd605aefa95650a6b8b773e32e6e52e58792d0dbf6736dd3406be",
|
|
1178
|
-
"src/integrity.ts": "
|
|
1178
|
+
"src/integrity.ts": "c1ef78d93f418f50b15178de939c21e480ed49420c003d75e1c344ecbb95ce5b",
|
|
1179
1179
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
1180
1180
|
"src/planner-kernel.ts": "5d3f5305fb59becb2cc97d8a03aca6b104f385805d91cc0410856e289dd6fbb1",
|
|
1181
1181
|
"src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
|
|
1182
1182
|
"src/planner-validators.ts": "99adeb65f579af923c11f21a8f55cfb9993b007fd8df59c5f764696b55379cfc",
|
|
1183
1183
|
"src/register.ts": "6a2f1c6b8df0dc9c8f45df8ec9fac2be29044e048b08c8ffe423410e94dadedd",
|
|
1184
1184
|
"src/release.ts": "7b0fe444ee81f476f53b9c21b6c347ef588ee616c60211419cf984132be768ba",
|
|
1185
|
-
"src/setup.ts": "
|
|
1185
|
+
"src/setup.ts": "8e69ffc4f49ad7240243728db3c0c495d2c4d66f04652cd735a0a90c79ca4246",
|
|
1186
1186
|
"src/telemetry-plan.ts": "cdb1e8bfff414f3ea81977388973da2cf6d89567d9e060dc9d5989a33600f153",
|
|
1187
1187
|
"src/topology.ts": "9779ee48e1c453abb76363163d4ab363463fa14e359571139f6d1f1fcc51ae2c",
|
|
1188
1188
|
"templates/agents/AGENTS.block.md": "c45d831e57976b874d2a6dc6e5e929b6e9ea3f928fe8c6683c2bab71676817c7",
|
|
1189
1189
|
"templates/agents/atdd-bun.integrity.test.ts": "dec6f6e5f65a08d9512fc703b367c163f8c9da1f6aa7fd466835c1ec30348a41",
|
|
1190
1190
|
"templates/agents/atdd/SKILL.md": "b621abe22851a75b30fc0e6c339a2c0a3ed6bbf78b8c17000a4ce301e05df96d",
|
|
1191
1191
|
"templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
|
|
1192
|
-
"templates/github/atdd-bun.yml": "
|
|
1192
|
+
"templates/github/atdd-bun.yml": "b54fe01661e7141ae70b6db881a4870c4a09c11b1be29ed8364ace35dc4e0564"
|
|
1193
1193
|
}
|
|
1194
1194
|
}
|
package/package.json
CHANGED
package/src/integrity.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { instructionPaths } from "./agent";
|
|
5
|
-
import { concreteProfiles } from "./enforce";
|
|
6
5
|
import { defaultHookPolicy, type HookPolicy } from "./hooks";
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -106,6 +105,10 @@ async function checkGenerated(root: string, packageRoot: string): Promise<Integr
|
|
|
106
105
|
return findings;
|
|
107
106
|
}
|
|
108
107
|
|
|
108
|
+
/** The operator's explicit profile list, or null when atdd-bun.yaml declares none. Deliberately not enabledProfiles():
|
|
109
|
+
* its absent-means-all default is right for execution and wrong for deciding whether a policy was ever declared. */
|
|
110
|
+
const explicitProfiles = (config: { profiles?: unknown }): string[] | null => Array.isArray(config.profiles) ? config.profiles.map(String) : null;
|
|
111
|
+
|
|
109
112
|
/** Names of the policy fields in `current` that are looser than in `base`. */
|
|
110
113
|
export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown }, current: Partial<HookPolicy> & { profiles?: unknown }): string[] {
|
|
111
114
|
const b = { ...defaultHookPolicy, ...base, worktrees: { ...defaultHookPolicy.worktrees, ...base.worktrees } }, c = { ...defaultHookPolicy, ...current, worktrees: { ...defaultHookPolicy.worktrees, ...current.worktrees } };
|
|
@@ -116,20 +119,41 @@ export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown
|
|
|
116
119
|
const removed = b.protected_branches.filter(x => !c.protected_branches.includes(x)), added = c.registry_paths.filter(x => !b.registry_paths.includes(x));
|
|
117
120
|
if (removed.length) out.push(`protected_branches drops ${removed.join(", ")}`);
|
|
118
121
|
if (added.length) out.push(`registry_paths adds ${added.join(", ")}`);
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
// Profiles: an absent list runs every profile (enabledProfiles) but governs none. The first explicit list is the
|
|
123
|
+
// adoption that establishes the governed set, not a drop. From then on, dropping a profile or removing the list
|
|
124
|
+
// (explicit → implicit → narrower would otherwise be a two-step bypass) is loosening a human approves.
|
|
125
|
+
const before = explicitProfiles(base), after = explicitProfiles(current);
|
|
126
|
+
if (before && !after) out.push(`profiles becomes implicit: the explicit list [${before.join(", ")}] was removed`);
|
|
127
|
+
const dropped = before && after ? before.filter(name => !after.includes(name)) : [];
|
|
122
128
|
if (dropped.length) out.push(`profiles drops ${dropped.join(", ")}`);
|
|
123
129
|
return out;
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
/** atdd-bun.yaml is not looser than on the branch being merged into. */
|
|
133
|
+
/** How to recover a baseline that cannot be resolved. A replaced tip is reachable from no branch, so only a fetch by its
|
|
134
|
+
* full object id brings it back (SHA-1 or SHA-256, any case); an abbreviated id cannot be fetched; a ref name can. */
|
|
135
|
+
export function baselineRestore(ref: string): string {
|
|
136
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(ref)) return `git fetch origin ${ref}, then re-run the check (a replaced tip is reachable from no branch, so a plain fetch does not bring it)`;
|
|
137
|
+
if (/^[0-9a-f]{4,63}$/i.test(ref)) return `set ATDD_BASE_REF to the full SHA of ${ref} (an abbreviated SHA cannot be fetched), fetch it with git fetch origin <full SHA>, then re-run the check`;
|
|
138
|
+
return `git fetch origin, then re-run the check`;
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
async function checkPolicy(root: string, base?: string, push = process.env.GITHUB_EVENT_NAME === "push"): Promise<IntegrityFinding[]> {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
// On a push, the generated CI passes the pre-push tip (github.event.before) as ATDD_BASE_REF, so a multi-commit push
|
|
143
|
+
// is judged as a whole: [docs, security] → no list → [docs] in one push cannot read as a first adoption.
|
|
144
|
+
const ref = base || process.env.ATDD_BASE_REF || (process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : "origin/HEAD");
|
|
145
|
+
const newBranch = /^0+$/.test(ref), resolved = newBranch ? "" : (await git(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`])).out;
|
|
146
|
+
let against: string;
|
|
147
|
+
// An explicit baseline (the pre-push tip, the merge queue's target) that cannot be resolved fails closed.
|
|
148
|
+
if (!newBranch && !resolved && (base || process.env.ATDD_BASE_REF)) return [{ file: "atdd-bun.yaml", detail: `cannot resolve the policy baseline ${ref} to judge this change against`, restore: baselineRestore(ref) }];
|
|
149
|
+
if (push) {
|
|
150
|
+
// A push is judged against the tip it replaced, directly, never a merge base: after a force push the merge base
|
|
151
|
+
// can predate the policy being removed. A new branch has no previous tip and is judged against its parent.
|
|
152
|
+
against = resolved && resolved !== (await git(root, ["rev-parse", "HEAD"])).out ? resolved : (await git(root, ["rev-parse", "--verify", "--quiet", "HEAD~1"])).out;
|
|
153
|
+
} else {
|
|
154
|
+
if (!resolved) return [];
|
|
155
|
+
against = (await git(root, ["merge-base", "HEAD", ref])).out;
|
|
156
|
+
}
|
|
133
157
|
if (!against) return [];
|
|
134
158
|
const read = async (text: string | null) => (text ? Bun.YAML.parse(text) ?? {} : {}) as Partial<HookPolicy>;
|
|
135
159
|
const before = await git(root, ["show", `${against}:atdd-bun.yaml`]), path = join(root, "atdd-bun.yaml");
|
package/src/setup.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { ciInit, ciStatus } from "./ci";
|
|
|
2
2
|
import { hooksStatus, installHooks } from "./hooks";
|
|
3
3
|
import { agentInit, agentStatus } from "./agent";
|
|
4
4
|
import { integrityInit, integrityStatus } from "./integrity";
|
|
5
|
+
import { concreteProfiles } from "./enforce";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** A new atdd-bun.yaml declares every profile explicitly. An absent list runs every profile but governs none, so
|
|
11
|
+
* without this a greenfield repository's first `profiles:` line could switch most of them off unreported. A
|
|
12
|
+
* brownfield repository trims the list before its first commit; after that, dropping one is a reported loosening.
|
|
13
|
+
* An existing atdd-bun.yaml is never touched. */
|
|
14
|
+
export async function policyInit(root = process.cwd()) {
|
|
15
|
+
const file = join(root, "atdd-bun.yaml");
|
|
16
|
+
if (existsSync(file)) return { ok: true, message: `${file} kept` };
|
|
17
|
+
await writeFile(file, `# Generated by atdd-bun init. The profiles this repository enforces; trim the list before the first commit to\n# adopt gradually. Once committed, removing a profile or the list is reported by the integrity check.\nprofiles: [${concreteProfiles.join(", ")}]\n`);
|
|
18
|
+
return { ok: true, message: file };
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
/** Install the package's opt-in local surfaces without touching unrelated
|
|
7
22
|
* workflows or hook paths. Dependency installation itself never calls this. */
|
|
@@ -21,5 +36,6 @@ export async function initializeRepository(root = process.cwd(), replace = false
|
|
|
21
36
|
const existingIntegrity = await integrityStatus(root);
|
|
22
37
|
const integrity = replace || !existingIntegrity.ok ? await integrityInit(root, replace) : existingIntegrity;
|
|
23
38
|
if (!integrity.ok) return { ok: false, message: `integrity test: ${integrity.message}` };
|
|
24
|
-
|
|
39
|
+
const policy = await policyInit(root);
|
|
40
|
+
return { ok: true, message: `hooks: ${hooks.message}\nCI workflow: ${ci.message}\nagent skill: ${agent.message}\nintegrity test: ${integrity.message}\npolicy: ${policy.message}` };
|
|
25
41
|
}
|
|
@@ -9,6 +9,11 @@ jobs:
|
|
|
9
9
|
atdd-bun:
|
|
10
10
|
name: atdd-bun
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
|
+
env:
|
|
13
|
+
# The policy baseline for every step (the integrity step and the generated integrity test alike): on a push, the
|
|
14
|
+
# pre-push tip, so a multi-commit push is judged as a whole; in the merge queue, the queue's target (GitHub sets
|
|
15
|
+
# no GITHUB_BASE_REF there); on a pull request, GITHUB_BASE_REF.
|
|
16
|
+
ATDD_BASE_REF: ${{ github.event_name == 'push' && github.event.before || github.event_name == 'merge_group' && github.event.merge_group.base_sha || '' }}
|
|
12
17
|
steps:
|
|
13
18
|
- uses: actions/checkout@v4
|
|
14
19
|
with: { fetch-depth: 0 }
|