@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 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. Removing a profile loosens `atdd-bun.yaml`, so the integrity
81
- check reports it against the base branch until a human approves the change.
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.0",
2
+ "version": "0.7.2",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "32ef74f2a1f29f709638ff08c21d247e00761b62ae056883dc701e4a4d51a518",
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": "009b5f23ec3240cdc2cb133f942995e48f494edaa0259053bca15283134be3ea",
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": "0a6f1a9aeff31dcf17f6bd036168b909c1841c11bbbb452105d8debf64a6bc78",
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": "12b2e23863b64197e0f2f6f2976fdb5cb06f79662122542bf8d2f7ad41337be9"
1192
+ "templates/github/atdd-bun.yml": "b54fe01661e7141ae70b6db881a4870c4a09c11b1be29ed8364ace35dc4e0564"
1193
1193
  }
1194
1194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
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
- // Deactivating a profile stops enforcing it; the operator may do it, as a change a human approves.
120
- const active = (config: { profiles?: unknown }): string[] => Array.isArray(config.profiles) ? config.profiles.map(String) : concreteProfiles;
121
- const dropped = active(base).filter(name => !active(current).includes(name));
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
- const ref = base ?? process.env.ATDD_BASE_REF ?? (process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : "origin/HEAD");
129
- if ((await git(root, ["rev-parse", "--verify", "--quiet", ref])).code) return [];
130
- let against = (await git(root, ["merge-base", "HEAD", ref])).out;
131
- // A CI push to the base branch has nothing to merge into: judge the pushed commit against its parent.
132
- if (push && against === (await git(root, ["rev-parse", "HEAD"])).out) against = (await git(root, ["rev-parse", "--verify", "--quiet", "HEAD~1"])).out;
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
- return { ok: true, message: `hooks: ${hooks.message}\nCI workflow: ${ci.message}\nagent skill: ${agent.message}\nintegrity test: ${integrity.message}` };
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 }