@ikon85/agent-workflow-kit 0.14.0 → 0.15.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.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: census-update
3
+ description: "Build, refresh, or check an optional project-local census. Use when a user invokes census-update, asks to establish a counted surface census, or needs to reconcile census drift; scan facts in the current repository, guide only ambiguous decisions, and activate a verified candidate transactionally."
4
+ ---
5
+
6
+ # Census Update
7
+
8
+ Build and maintain the current repository's consumer-owned census through the
9
+ public API in `scripts/census/index.mjs`. Keep this skill a thin coordinator:
10
+ do not copy, rename, or reimplement the scanner, state, delta, fingerprint, or
11
+ transaction logic.
12
+
13
+ ## Boundaries
14
+
15
+ - Work only in the current repository. Never inspect another project, aggregate
16
+ cross-project findings, learn shared recipes from other repositories, or
17
+ propose an upstream kit change.
18
+ - Treat code as the source of facts. Never ask the user which files, paths, or
19
+ patterns exist.
20
+ - Ask only for one genuinely ambiguous decision at a time. Include a concise
21
+ recommendation and the evidence behind it.
22
+ - Keep generated scanners, tests, profiles, decisions, and the active census in
23
+ the consumer repository. Do not add a dependency without explicit approval.
24
+
25
+ ## Public mechanism
26
+
27
+ Import only the stable exports from `scripts/census/index.mjs`:
28
+ `scanCensus`, `serializeCensus`, `fingerprintCensus`,
29
+ `CENSUS_BUILDER_VERSION`, `diffCensus`, `CENSUS_STATES`, `CENSUS_VERDICTS`,
30
+ `resolveCensusState`, `activateCensus`, and `CensusTransactionError`.
31
+
32
+ ## Consumer files
33
+
34
+ First adopt an existing, explicitly documented census path convention when the
35
+ repository has one. Otherwise use `.census/profile.json` for the consumer-owned
36
+ profile and `.census/active.json` for the verified snapshot. A missing default
37
+ profile is not an error: report `bootstrap` and stage it only after facts and
38
+ decisions are ready rather than silently enabling a gate.
39
+
40
+ The deterministic profile schema is:
41
+
42
+ ```json
43
+ {
44
+ "schemaVersion": 1,
45
+ "enabled": true,
46
+ "decisions": [
47
+ { "family": "name", "status": "verdict", "evidence": "fact", "justification": "required when not relevant" }
48
+ ],
49
+ "localScanners": [
50
+ { "surface": "family", "module": "repo-relative path", "export": "function", "test": "repo-relative test path" }
51
+ ],
52
+ "overrides": [
53
+ { "scope": "this change", "reason": "visible display-only reason" }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ Keep all three arrays present and order their records by family or surface, then
59
+ path. Preserve extra consumer-owned keys when rewriting an adopted profile.
60
+ Pass `decisions` to `scanCensus` as `behaviorFamilies` after mapping `family` to
61
+ `name`. Put the unchanged decision, local-scanner, and override records in a
62
+ visible `profileReport` beside the scanned snapshot. Derive snapshot state with
63
+ `resolveCensusState`; never assign `current` as a literal shortcut.
64
+
65
+ Local-scanner records configure the verifier passed to `activateCensus`: run
66
+ the named focused test, import the named repository-local export, and require
67
+ its result to include the recorded surface. Both `module` and `test` must be
68
+ repository-relative, contained regular files; reject absolute paths, `..`
69
+ escapes, and symlinks before executing or importing either one. Compare surface
70
+ families with the previous active snapshot; a new family without that passing
71
+ proof is still open for state resolution even when Git tracking lets the base
72
+ scan enumerate it. Overrides are report input only and are never passed into
73
+ scanning, fingerprinting, verification, or state resolution.
74
+
75
+ ## Workflow
76
+
77
+ 1. **Check.** Read the local profile and active census, if present. Report a
78
+ missing profile as `bootstrap`; otherwise derive one
79
+ of `disabled`, `bootstrap`, `current`, `refresh_required`, `updating`, or
80
+ `failed` through the public API. An explicit opt-out is disabled. Do not
81
+ write during this step.
82
+ 2. **Scan facts.** Call `scanCensus` once with the current repository root.
83
+ Product code and production configuration form the surface denominator;
84
+ tests and docs remain evidence. Let the scanner exclude secrets, ignored,
85
+ generated, and vendored content. Never read excluded content to explain a
86
+ result.
87
+ 3. **Show the delta.** For an active census, call `diffCensus` and show only
88
+ added, changed, removed, and open names. Do not dump the full scan unless the
89
+ user asks. For bootstrap, show the discovered families and counts.
90
+ 4. **Resolve ambiguity.** Recommend a decision for each ambiguous behavior or
91
+ surface and ask separately. Record the user's decision with its evidence.
92
+ `nicht relevant` requires a durable justification. Never infer it silently. <!-- language-census: ok -->
93
+ 5. **Handle unknown patterns locally.** Keep an unknown surface `offen`. It can
94
+ become `abgedeckt` only after this repository contains a small local scanner
95
+ and a focused passing test for that pattern. Run that test before rescanning.
96
+ 6. **Verify a candidate.** Build the candidate from the fresh scan and recorded
97
+ decisions. Fail verification when any surface or behavior remains `offen`,
98
+ when a required local scanner test fails, or when the candidate fingerprints
99
+ do not describe the current repository.
100
+ 7. **Activate.** Call `activateCensus` with a real verifier so it stages, verifies,
101
+ and atomically swaps under its local lock. On `CensusTransactionError`, report
102
+ `updating` or `failed` and keep the previous active census authoritative.
103
+ 8. **Prove the result.** Report surface coverage as `X of Y`, then render the
104
+ behavior overview separately. State the resulting census state and the
105
+ builder version.
106
+
107
+ ## Decision rules
108
+
109
+ - Store a justified `nicht relevant` verdict as a visible decision beside the <!-- language-census: ok -->
110
+ candidate; do not erase the family from the overview.
111
+ - A change-local override may suppress a known mechanical false-positive in
112
+ the displayed delta only when its reason and scope are visible. It must not
113
+ alter scanner facts, fingerprints, `offen` verdicts, or state resolution, so
114
+ real drift can never become `current` through an override.
115
+ - Any unexpected product area stays `offen` and therefore prevents `current`,
116
+ even if every known area is covered.
117
+ - Facts discovered after a decision invalidate that decision's stale premise;
118
+ rescan and ask again only if the remaining choice is genuinely ambiguous.
119
+
120
+ ## No-write and recovery proof
121
+
122
+ Before activation, compare the fresh candidate with the active census using
123
+ the public fingerprints and deterministic `serializeCensus` output. If the
124
+ repository and decisions are unchanged and the derived state is `current`,
125
+ report `current` and do not call `activateCensus` or write any file.
126
+
127
+ If scanning, a local test, verification, or activation fails, report `failed`,
128
+ discard the candidate when safe, and confirm that the previous active census
129
+ bytes are unchanged. Never manufacture `current` after an error.
130
+
131
+ ## Final report
132
+
133
+ Return only the useful audit trail:
134
+
135
+ - state and builder version;
136
+ - compact delta;
137
+ - surface coverage `X of Y`;
138
+ - separate behavior overview;
139
+ - visible `nicht relevant` justifications and active override, if any; <!-- language-census: ok -->
140
+ - local scanner tests run and the transaction/no-write result.
@@ -17,6 +17,7 @@ Use this section as the entry-point map for agent-assisted work. The individual
17
17
  - **A design question needs a runnable answer (state, business logic, a UI you have to see):** spike it with `prototype`, then fold what you learned back in.
18
18
  - **Multi-session build from a PRD or issue:** use `implement` to drive `tdd` end-to-end, one red-green slice at a time.
19
19
  - **Implementation slice:** use `tdd` for one behavior at a time: RED, GREEN, then refactor.
20
+ - **Build or refresh a project-local census:** use `census-update` to scan facts, resolve ambiguous decisions one at a time, and transactionally activate a counted census.
20
21
  - **Finished slice:** use `wrapup` to prepare the branch, PR, and cleanup steps your repo expects.
21
22
  - **Kit release:** use `kit-release` to derive the shipped delta, confirm Semver, regenerate the manifest, run all gates, and then hand landing to `wrapup`.
22
23
  - **Kit update:** use `kit-update` to preview and transactionally apply a parity-verified scoped release without overwriting local modifications.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: census-update
3
+ description: "Build, refresh, or check an optional project-local census. Use when a user invokes census-update, asks to establish a counted surface census, or needs to reconcile census drift; scan facts in the current repository, guide only ambiguous decisions, and activate a verified candidate transactionally."
4
+ ---
5
+
6
+ # Census Update
7
+
8
+ Build and maintain the current repository's consumer-owned census through the
9
+ public API in `scripts/census/index.mjs`. Keep this skill a thin coordinator:
10
+ do not copy, rename, or reimplement the scanner, state, delta, fingerprint, or
11
+ transaction logic.
12
+
13
+ ## Boundaries
14
+
15
+ - Work only in the current repository. Never inspect another project, aggregate
16
+ cross-project findings, learn shared recipes from other repositories, or
17
+ propose an upstream kit change.
18
+ - Treat code as the source of facts. Never ask the user which files, paths, or
19
+ patterns exist.
20
+ - Ask only for one genuinely ambiguous decision at a time. Include a concise
21
+ recommendation and the evidence behind it.
22
+ - Keep generated scanners, tests, profiles, decisions, and the active census in
23
+ the consumer repository. Do not add a dependency without explicit approval.
24
+
25
+ ## Public mechanism
26
+
27
+ Import only the stable exports from `scripts/census/index.mjs`:
28
+ `scanCensus`, `serializeCensus`, `fingerprintCensus`,
29
+ `CENSUS_BUILDER_VERSION`, `diffCensus`, `CENSUS_STATES`, `CENSUS_VERDICTS`,
30
+ `resolveCensusState`, `activateCensus`, and `CensusTransactionError`.
31
+
32
+ ## Consumer files
33
+
34
+ First adopt an existing, explicitly documented census path convention when the
35
+ repository has one. Otherwise use `.census/profile.json` for the consumer-owned
36
+ profile and `.census/active.json` for the verified snapshot. A missing default
37
+ profile is not an error: report `bootstrap` and stage it only after facts and
38
+ decisions are ready rather than silently enabling a gate.
39
+
40
+ The deterministic profile schema is:
41
+
42
+ ```json
43
+ {
44
+ "schemaVersion": 1,
45
+ "enabled": true,
46
+ "decisions": [
47
+ { "family": "name", "status": "verdict", "evidence": "fact", "justification": "required when not relevant" }
48
+ ],
49
+ "localScanners": [
50
+ { "surface": "family", "module": "repo-relative path", "export": "function", "test": "repo-relative test path" }
51
+ ],
52
+ "overrides": [
53
+ { "scope": "this change", "reason": "visible display-only reason" }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ Keep all three arrays present and order their records by family or surface, then
59
+ path. Preserve extra consumer-owned keys when rewriting an adopted profile.
60
+ Pass `decisions` to `scanCensus` as `behaviorFamilies` after mapping `family` to
61
+ `name`. Put the unchanged decision, local-scanner, and override records in a
62
+ visible `profileReport` beside the scanned snapshot. Derive snapshot state with
63
+ `resolveCensusState`; never assign `current` as a literal shortcut.
64
+
65
+ Local-scanner records configure the verifier passed to `activateCensus`: run
66
+ the named focused test, import the named repository-local export, and require
67
+ its result to include the recorded surface. Both `module` and `test` must be
68
+ repository-relative, contained regular files; reject absolute paths, `..`
69
+ escapes, and symlinks before executing or importing either one. Compare surface
70
+ families with the previous active snapshot; a new family without that passing
71
+ proof is still open for state resolution even when Git tracking lets the base
72
+ scan enumerate it. Overrides are report input only and are never passed into
73
+ scanning, fingerprinting, verification, or state resolution.
74
+
75
+ ## Workflow
76
+
77
+ 1. **Check.** Read the local profile and active census, if present. Report a
78
+ missing profile as `bootstrap`; otherwise derive one
79
+ of `disabled`, `bootstrap`, `current`, `refresh_required`, `updating`, or
80
+ `failed` through the public API. An explicit opt-out is disabled. Do not
81
+ write during this step.
82
+ 2. **Scan facts.** Call `scanCensus` once with the current repository root.
83
+ Product code and production configuration form the surface denominator;
84
+ tests and docs remain evidence. Let the scanner exclude secrets, ignored,
85
+ generated, and vendored content. Never read excluded content to explain a
86
+ result.
87
+ 3. **Show the delta.** For an active census, call `diffCensus` and show only
88
+ added, changed, removed, and open names. Do not dump the full scan unless the
89
+ user asks. For bootstrap, show the discovered families and counts.
90
+ 4. **Resolve ambiguity.** Recommend a decision for each ambiguous behavior or
91
+ surface and ask separately. Record the user's decision with its evidence.
92
+ `nicht relevant` requires a durable justification. Never infer it silently. <!-- language-census: ok -->
93
+ 5. **Handle unknown patterns locally.** Keep an unknown surface `offen`. It can
94
+ become `abgedeckt` only after this repository contains a small local scanner
95
+ and a focused passing test for that pattern. Run that test before rescanning.
96
+ 6. **Verify a candidate.** Build the candidate from the fresh scan and recorded
97
+ decisions. Fail verification when any surface or behavior remains `offen`,
98
+ when a required local scanner test fails, or when the candidate fingerprints
99
+ do not describe the current repository.
100
+ 7. **Activate.** Call `activateCensus` with a real verifier so it stages, verifies,
101
+ and atomically swaps under its local lock. On `CensusTransactionError`, report
102
+ `updating` or `failed` and keep the previous active census authoritative.
103
+ 8. **Prove the result.** Report surface coverage as `X of Y`, then render the
104
+ behavior overview separately. State the resulting census state and the
105
+ builder version.
106
+
107
+ ## Decision rules
108
+
109
+ - Store a justified `nicht relevant` verdict as a visible decision beside the <!-- language-census: ok -->
110
+ candidate; do not erase the family from the overview.
111
+ - A change-local override may suppress a known mechanical false-positive in
112
+ the displayed delta only when its reason and scope are visible. It must not
113
+ alter scanner facts, fingerprints, `offen` verdicts, or state resolution, so
114
+ real drift can never become `current` through an override.
115
+ - Any unexpected product area stays `offen` and therefore prevents `current`,
116
+ even if every known area is covered.
117
+ - Facts discovered after a decision invalidate that decision's stale premise;
118
+ rescan and ask again only if the remaining choice is genuinely ambiguous.
119
+
120
+ ## No-write and recovery proof
121
+
122
+ Before activation, compare the fresh candidate with the active census using
123
+ the public fingerprints and deterministic `serializeCensus` output. If the
124
+ repository and decisions are unchanged and the derived state is `current`,
125
+ report `current` and do not call `activateCensus` or write any file.
126
+
127
+ If scanning, a local test, verification, or activation fails, report `failed`,
128
+ discard the candidate when safe, and confirm that the previous active census
129
+ bytes are unchanged. Never manufacture `current` after an error.
130
+
131
+ ## Final report
132
+
133
+ Return only the useful audit trail:
134
+
135
+ - state and builder version;
136
+ - compact delta;
137
+ - surface coverage `X of Y`;
138
+ - separate behavior overview;
139
+ - visible `nicht relevant` justifications and active override, if any; <!-- language-census: ok -->
140
+ - local scanner tests run and the transaction/no-write result.
@@ -17,6 +17,7 @@ Use this section as the entry-point map for agent-assisted work. The individual
17
17
  - **A design question needs a runnable answer (state, business logic, a UI you have to see):** spike it with `prototype`, then fold what you learned back in.
18
18
  - **Multi-session build from a PRD or issue:** use `implement` to drive `tdd` end-to-end, one red-green slice at a time.
19
19
  - **Implementation slice:** use `tdd` for one behavior at a time: RED, GREEN, then refactor.
20
+ - **Build or refresh a project-local census:** use `census-update` to scan facts, resolve ambiguous decisions one at a time, and transactionally activate a counted census.
20
21
  - **Finished slice:** use `wrapup` to prepare the branch, PR, and cleanup steps your repo expects.
21
22
  - **Kit release:** use `kit-release` to derive the shipped delta, confirm Semver, regenerate the manifest, run all gates, and then hand landing to `wrapup`.
22
23
  - **Kit update:** use `kit-update` to preview and transactionally apply a parity-verified scoped release without overwriting local modifications.
@@ -107,6 +107,16 @@
107
107
  ],
108
108
  "provenance": "matt-pocock"
109
109
  },
110
+ "census-update": {
111
+ "class": "generic",
112
+ "publish": true,
113
+ "entryPoint": true,
114
+ "surfaces": [
115
+ "claude",
116
+ "codex"
117
+ ],
118
+ "provenance": "own"
119
+ },
110
120
  "board-to-waves": {
111
121
  "class": "generic",
112
122
  "publish": true,
package/PROVENANCE.md CHANGED
@@ -32,8 +32,8 @@ carries Chase's `THIRD-PARTY-NOTICES.md`.
32
32
  ## Own work (MIT, Copyright (c) 2026 Niko (iKon85))
33
33
 
34
34
  retro, wrapup, spec-self-critique, board-to-waves, verify-spike, decision-gate,
35
- codex-adapter-sync, setup-pre-commit, code-review, orchestrate-wave — covered by
36
- the root LICENSE.
35
+ codex-adapter-sync, setup-pre-commit, code-review, orchestrate-wave,
36
+ census-update — covered by the root LICENSE.
37
37
  `setup-pre-commit` was rewritten from Matt Pocock's husky-based skill to a
38
38
  zero-dep native git `core.hooksPath` scaffold; nothing of the original
39
39
  mechanic remains (courtesy attribution).
package/README.md CHANGED
@@ -332,6 +332,13 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
332
332
 
333
333
  ## Release notes
334
334
 
335
+ ### 0.15.0
336
+
337
+ - added: `.agents/skills/census-update/SKILL.md`
338
+ - added: `.claude/skills/census-update/SKILL.md`
339
+ - changed: `.agents/skills/setup-workflow/workflow-overview.md`
340
+ - changed: `.claude/skills/setup-workflow/workflow-overview.md`
341
+
335
342
  ### 0.14.0
336
343
 
337
344
  - added: `scripts/census/delta.mjs`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.14.0",
2
+ "kitVersion": "0.15.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -37,6 +37,15 @@
37
37
  "mode": 420,
38
38
  "origin": "kit"
39
39
  },
40
+ {
41
+ "path": ".agents/skills/census-update/SKILL.md",
42
+ "kind": "skill",
43
+ "ownerSkill": "census-update",
44
+ "surface": "codex",
45
+ "sha256": "3a04a2667005cae51cfad7952ff85fe43ba1ae390458b4ce746e23f6e872b3fb",
46
+ "mode": 420,
47
+ "origin": "kit"
48
+ },
40
49
  {
41
50
  "path": ".agents/skills/code-review/SKILL.md",
42
51
  "kind": "skill",
@@ -582,7 +591,7 @@
582
591
  "kind": "skill",
583
592
  "ownerSkill": "setup-workflow",
584
593
  "surface": "codex",
585
- "sha256": "6ce3954e4f94d112801cba0d145e4d3b6f101aca5e50e6b1f9794aa0288881a9",
594
+ "sha256": "b83fa87dc0226f21e01d31c91840b78d1ccad0a92e646de203fc3c003e9098f3",
586
595
  "mode": 420,
587
596
  "origin": "kit"
588
597
  },
@@ -893,6 +902,15 @@
893
902
  "mode": 420,
894
903
  "origin": "kit"
895
904
  },
905
+ {
906
+ "path": ".claude/skills/census-update/SKILL.md",
907
+ "kind": "skill",
908
+ "ownerSkill": "census-update",
909
+ "surface": "claude",
910
+ "sha256": "3a04a2667005cae51cfad7952ff85fe43ba1ae390458b4ce746e23f6e872b3fb",
911
+ "mode": 420,
912
+ "origin": "kit"
913
+ },
896
914
  {
897
915
  "path": ".claude/skills/code-review/SKILL.md",
898
916
  "kind": "skill",
@@ -1564,7 +1582,7 @@
1564
1582
  "kind": "skill",
1565
1583
  "ownerSkill": "setup-workflow",
1566
1584
  "surface": "claude",
1567
- "sha256": "6ce3954e4f94d112801cba0d145e4d3b6f101aca5e50e6b1f9794aa0288881a9",
1585
+ "sha256": "b83fa87dc0226f21e01d31c91840b78d1ccad0a92e646de203fc3c003e9098f3",
1568
1586
  "mode": 420,
1569
1587
  "origin": "kit"
1570
1588
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,440 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { access, lstat, mkdtemp, mkdir, readFile, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import {
10
+ CENSUS_VERDICTS,
11
+ activateCensus,
12
+ diffCensus,
13
+ fingerprintCensus,
14
+ resolveCensusState,
15
+ scanCensus,
16
+ serializeCensus,
17
+ } from './census/index.mjs';
18
+
19
+ const ROOT = new URL('../', import.meta.url);
20
+ const exec = promisify(execFile);
21
+
22
+ async function text(path) {
23
+ return readFile(new URL(path, ROOT), 'utf8');
24
+ }
25
+
26
+ async function makeBrownfield() {
27
+ const root = await mkdtemp(join(tmpdir(), 'awk-census-update-'));
28
+ await mkdir(join(root, 'apps/api/src'), { recursive: true });
29
+ await mkdir(join(root, 'apps/web/src'), { recursive: true });
30
+ await writeFile(join(root, 'package.json'), '{"name":"brownfield","type":"module"}\n');
31
+ await writeFile(join(root, 'apps/api/src/index.mjs'), 'export const api = true;\n');
32
+ await writeFile(join(root, 'apps/web/src/index.mjs'), 'export const web = true;\n');
33
+ await exec('git', ['init', '--quiet'], { cwd: root });
34
+ await exec('git', ['add', '.'], { cwd: root });
35
+ return root;
36
+ }
37
+
38
+ const PROFILE_RELATIVE_PATH = '.census/profile.json';
39
+ const ACTIVE_RELATIVE_PATH = '.census/active.json';
40
+
41
+ async function readJson(path) {
42
+ return JSON.parse(await readFile(path, 'utf8'));
43
+ }
44
+
45
+ function profileBehaviorFamilies(profile) {
46
+ return profile.decisions.map(({ family, status }) => ({ name: family, status }));
47
+ }
48
+
49
+ async function containedRegularPath(root, localPath) {
50
+ if (typeof localPath !== 'string' || !localPath || isAbsolute(localPath)) return null;
51
+ const target = resolve(root, localPath);
52
+ const lexical = relative(resolve(root), target);
53
+ if (lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) return null;
54
+ const stats = await lstat(target);
55
+ if (!stats.isFile() || stats.isSymbolicLink()) return null;
56
+ const canonicalRoot = await realpath(root);
57
+ const canonical = await realpath(target);
58
+ const canonicalRelative = relative(canonicalRoot, canonical);
59
+ if (canonicalRelative === '..' || canonicalRelative.startsWith(`..${sep}`)
60
+ || isAbsolute(canonicalRelative)) return null;
61
+ return target;
62
+ }
63
+
64
+ async function runLocalScannerProof(root, record) {
65
+ const modulePath = await containedRegularPath(root, record.module);
66
+ const testPath = await containedRegularPath(root, record.test);
67
+ if (!modulePath || !testPath) return false;
68
+ await exec('node', ['--test', record.test], { cwd: root });
69
+ const scanner = await import(`${pathToFileURL(modulePath).href}?proof=${Date.now()}`);
70
+ return scanner[record.export]().includes(record.surface);
71
+ }
72
+
73
+ async function runConsumerCensus(root) {
74
+ const profilePath = join(root, PROFILE_RELATIVE_PATH);
75
+ const activePath = join(root, ACTIVE_RELATIVE_PATH);
76
+ const profile = await readJson(profilePath);
77
+ let previous = null;
78
+ try {
79
+ previous = await readJson(activePath);
80
+ } catch (error) {
81
+ if (error.code !== 'ENOENT') throw error;
82
+ }
83
+
84
+ const scan = await scanCensus({
85
+ repoRoot: root,
86
+ enabled: profile.enabled,
87
+ hasActive: previous !== null,
88
+ behaviorFamilies: profileBehaviorFamilies(profile),
89
+ });
90
+ const previousSurfaces = new Set(previous?.families.surfaces.map(({ name }) => name) ?? []);
91
+ const unexpected = previous === null
92
+ ? []
93
+ : scan.families.surfaces.filter(({ name }) => !previousSurfaces.has(name));
94
+ const scannerBySurface = new Map(profile.localScanners.map((record) => [record.surface, record]));
95
+ const missingScanner = unexpected.some(({ name }) => !scannerBySurface.has(name));
96
+ const hasOpen = missingScanner || [...scan.families.surfaces, ...scan.families.behaviors]
97
+ .some(({ status }) => status === CENSUS_VERDICTS.open);
98
+ const candidate = {
99
+ ...scan,
100
+ profileReport: {
101
+ decisions: profile.decisions,
102
+ localScanners: profile.localScanners,
103
+ overrides: profile.overrides,
104
+ },
105
+ state: resolveCensusState({ enabled: profile.enabled, hasActive: true, hasOpen }),
106
+ };
107
+
108
+ if (previous !== null && serializeCensus(candidate) === serializeCensus(previous)) {
109
+ return { activated: false, activePath, candidate, profilePath };
110
+ }
111
+
112
+ await activateCensus({
113
+ activePath,
114
+ candidate,
115
+ verify: async (staged) => {
116
+ if (staged.state !== 'current') return false;
117
+ for (const { name } of unexpected) {
118
+ const record = scannerBySurface.get(name);
119
+ if (!record || !await runLocalScannerProof(root, record)) return false;
120
+ }
121
+ return staged.profileReport.decisions.every((decision) => (
122
+ decision.status !== CENSUS_VERDICTS.notRelevant || Boolean(decision.justification)
123
+ ));
124
+ },
125
+ });
126
+ return { activated: true, activePath, candidate, profilePath };
127
+ }
128
+
129
+ test('census-update is a published dual-surface own-work entry point', async () => {
130
+ const [source, mirror, manifestText, provenance, claudeOverview, codexOverview] = await Promise.all([
131
+ text('.claude/skills/census-update/SKILL.md'),
132
+ text('.agents/skills/census-update/SKILL.md'),
133
+ text('.claude/skills/skill-manifest.json'),
134
+ text('PROVENANCE.md'),
135
+ text('.claude/skills/setup-workflow/workflow-overview.md'),
136
+ text('.agents/skills/setup-workflow/workflow-overview.md'),
137
+ ]);
138
+ const entry = JSON.parse(manifestText).skills['census-update'];
139
+
140
+ assert.deepEqual(entry, {
141
+ class: 'generic',
142
+ publish: true,
143
+ entryPoint: true,
144
+ surfaces: ['claude', 'codex'],
145
+ provenance: 'own',
146
+ });
147
+ assert.equal(source, mirror);
148
+ assert.match(provenance, /Own work[\s\S]*census-update/);
149
+ assert.match(claudeOverview, /`census-update`/);
150
+ assert.match(codexOverview, /`census-update`/);
151
+ });
152
+
153
+ test('census-update coordinates the stable foundation API without a second engine', async () => {
154
+ const skill = await text('.claude/skills/census-update/SKILL.md');
155
+ for (const name of [
156
+ 'scanCensus', 'serializeCensus', 'fingerprintCensus', 'CENSUS_BUILDER_VERSION',
157
+ 'diffCensus', 'CENSUS_STATES', 'CENSUS_VERDICTS', 'resolveCensusState',
158
+ 'activateCensus', 'CensusTransactionError',
159
+ ]) {
160
+ assert.match(skill, new RegExp(`\\b${name}\\b`), `${name} must remain the public mechanism`);
161
+ }
162
+ assert.match(skill, /do not copy, rename, or reimplement/i);
163
+ assert.match(skill, /Never ask the user which files, paths, or\s+patterns exist/i);
164
+ assert.match(skill, /recommendation and the evidence/i);
165
+ assert.match(skill, /`nicht relevant` requires a durable justification/i);
166
+ assert.match(skill, /override[\s\S]*must not[\s\S]*alter scanner facts/i);
167
+ assert.match(skill, /Work only in the current repository/i);
168
+ assert.match(skill, /Never[\s\S]*propose an upstream kit change/i);
169
+ assert.match(skill, /\.census\/profile\.json/);
170
+ assert.match(skill, /\.census\/active\.json/);
171
+ assert.match(skill, /schemaVersion/);
172
+ });
173
+
174
+ test('brownfield bootstrap activates real surface coverage with a separate behavior overview', async () => {
175
+ const root = await makeBrownfield();
176
+ const profilePath = join(root, PROFILE_RELATIVE_PATH);
177
+ try {
178
+ await assert.rejects(access(profilePath), (error) => error.code === 'ENOENT');
179
+ const facts = await scanCensus({ repoRoot: root, enabled: true });
180
+ assert.equal(facts.state, 'bootstrap');
181
+ await mkdir(join(root, '.census'), { recursive: true });
182
+ await writeFile(profilePath, `${JSON.stringify({
183
+ schemaVersion: 1,
184
+ enabled: true,
185
+ decisions: [
186
+ { family: 'authentication', status: CENSUS_VERDICTS.covered, evidence: 'apps/api/src' },
187
+ {
188
+ family: 'email-delivery',
189
+ status: CENSUS_VERDICTS.notRelevant,
190
+ justification: 'The product sends no email.',
191
+ },
192
+ ],
193
+ localScanners: [],
194
+ overrides: [],
195
+ })}\n`);
196
+
197
+ const { activePath, candidate } = await runConsumerCensus(root);
198
+ const persisted = await readJson(activePath);
199
+ const covered = persisted.families.surfaces
200
+ .filter(({ status }) => status === CENSUS_VERDICTS.covered).length;
201
+
202
+ assert.equal(candidate.state, 'current');
203
+ assert.equal(persisted.state, 'current');
204
+ assert.equal(`${covered} of ${persisted.families.surfaces.length}`, '3 of 3');
205
+ assert.deepEqual(persisted.families.behaviors, [
206
+ { name: 'authentication', status: 'abgedeckt', type: 'behavior' },
207
+ { name: 'email-delivery', status: 'nicht relevant', type: 'behavior' },
208
+ ]);
209
+ assert.equal(persisted.profileReport.decisions[1].justification, 'The product sends no email.');
210
+ } finally {
211
+ await rm(root, { recursive: true, force: true });
212
+ }
213
+ });
214
+
215
+ test('a second unchanged current run performs no activation write', async () => {
216
+ const root = await makeBrownfield();
217
+ try {
218
+ await mkdir(join(root, '.census'), { recursive: true });
219
+ await writeFile(join(root, PROFILE_RELATIVE_PATH), `${JSON.stringify({
220
+ schemaVersion: 1,
221
+ enabled: true,
222
+ decisions: [],
223
+ localScanners: [],
224
+ overrides: [],
225
+ })}\n`);
226
+ const first = await runConsumerCensus(root);
227
+ assert.equal(first.activated, true);
228
+ const { activePath } = first;
229
+ const beforeBytes = await readFile(activePath, 'utf8');
230
+ const beforeStat = await stat(activePath);
231
+
232
+ const second = await runConsumerCensus(root);
233
+
234
+ assert.equal(second.activated, false);
235
+ assert.equal(await readFile(activePath, 'utf8'), beforeBytes);
236
+ assert.equal((await stat(activePath)).mtimeMs, beforeStat.mtimeMs);
237
+ } finally {
238
+ await rm(root, { recursive: true, force: true });
239
+ }
240
+ });
241
+
242
+ test('an unexpected surface stays open despite not-relevant decisions and a change-local override', async () => {
243
+ const root = await makeBrownfield();
244
+ try {
245
+ await mkdir(join(root, '.census'), { recursive: true });
246
+ await writeFile(join(root, PROFILE_RELATIVE_PATH), `${JSON.stringify({
247
+ schemaVersion: 1,
248
+ enabled: true,
249
+ decisions: [{
250
+ family: 'postal-delivery',
251
+ justification: 'The product sends no physical mail.',
252
+ status: CENSUS_VERDICTS.notRelevant,
253
+ }],
254
+ localScanners: [],
255
+ overrides: [{ reason: 'Known formatting-only path delta.', scope: 'this change' }],
256
+ })}\n`);
257
+ await runConsumerCensus(root);
258
+ await mkdir(join(root, 'services/payments/src'), { recursive: true });
259
+ await writeFile(join(root, 'services/payments/src/index.mjs'), 'export const payments = true;\n');
260
+ await exec('git', ['add', 'services'], { cwd: root });
261
+ const before = await readJson(join(root, ACTIVE_RELATIVE_PATH));
262
+
263
+ await assert.rejects(runConsumerCensus(root), (error) => error.state === 'failed');
264
+ const after = await readJson(join(root, ACTIVE_RELATIVE_PATH));
265
+ assert.equal(serializeCensus(after), serializeCensus(before));
266
+ assert.equal(after.profileReport.decisions[0].status, 'nicht relevant');
267
+ assert.equal(after.profileReport.decisions[0].justification, 'The product sends no physical mail.');
268
+ assert.equal(after.profileReport.overrides[0].scope, 'this change');
269
+ } finally {
270
+ await rm(root, { recursive: true, force: true });
271
+ }
272
+ });
273
+
274
+ test('a changed repository yields only compact path deltas and never reads a foreign repository', async () => {
275
+ const root = await makeBrownfield();
276
+ const foreign = await makeBrownfield();
277
+ try {
278
+ const reads = [];
279
+ const before = await scanCensus({ repoRoot: root });
280
+ await writeFile(join(root, 'apps/api/src/index.mjs'), 'export const api = "changed";\n');
281
+ const after = await scanCensus({
282
+ repoRoot: root,
283
+ readText: async (path) => {
284
+ reads.push(path);
285
+ return readFile(path, 'utf8');
286
+ },
287
+ });
288
+
289
+ assert.deepEqual(diffCensus(before, after), {
290
+ added: [],
291
+ changed: ['apps/api/src/index.mjs'],
292
+ open: [],
293
+ removed: [],
294
+ });
295
+ assert.ok(reads.length > 0);
296
+ assert.ok(reads.every((path) => path.startsWith(`${root}/`)));
297
+ assert.ok(reads.every((path) => !path.startsWith(`${foreign}/`)));
298
+ } finally {
299
+ await rm(root, { recursive: true, force: true });
300
+ await rm(foreign, { recursive: true, force: true });
301
+ }
302
+ });
303
+
304
+ test('an injected activation failure byte-preserves the active census', async () => {
305
+ const root = await makeBrownfield();
306
+ const activePath = join(root, '.census/active.json');
307
+ const previous = '{"generation":"known-good"}\n';
308
+ try {
309
+ await mkdir(join(root, '.census'), { recursive: true });
310
+ await writeFile(activePath, previous);
311
+ const candidate = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
312
+
313
+ await assert.rejects(
314
+ activateCensus({
315
+ activePath,
316
+ candidate,
317
+ verify: async () => true,
318
+ renameCandidate: async () => { throw new Error('injected swap failure'); },
319
+ }),
320
+ (error) => error.state === 'failed',
321
+ );
322
+ assert.equal(await readFile(activePath, 'utf8'), previous);
323
+ } finally {
324
+ await rm(root, { recursive: true, force: true });
325
+ }
326
+ });
327
+
328
+ test('an unknown pattern activates only after a repository-local scanner test passes', async () => {
329
+ const root = await makeBrownfield();
330
+ try {
331
+ await mkdir(join(root, '.census'), { recursive: true });
332
+ const profilePath = join(root, PROFILE_RELATIVE_PATH);
333
+ await writeFile(profilePath, `${JSON.stringify({
334
+ schemaVersion: 1,
335
+ enabled: true,
336
+ decisions: [],
337
+ localScanners: [],
338
+ overrides: [],
339
+ })}\n`);
340
+ const { activePath } = await runConsumerCensus(root);
341
+ const previous = await readFile(activePath, 'utf8');
342
+ await mkdir(join(root, 'services/payments/src'), { recursive: true });
343
+ await writeFile(join(root, 'services/payments/src/index.mjs'), 'export const payments = true;\n');
344
+ await exec('git', ['add', 'services'], { cwd: root });
345
+
346
+ await assert.rejects(runConsumerCensus(root), (error) => error.state === 'failed');
347
+ assert.equal(await readFile(activePath, 'utf8'), previous);
348
+
349
+ await mkdir(join(root, 'scripts/census-local'), { recursive: true });
350
+ await mkdir(join(root, 'test'), { recursive: true });
351
+ await writeFile(
352
+ join(root, 'scripts/census-local/scan-services.mjs'),
353
+ "export const scanServices = () => ['services/payments/src'];\n",
354
+ );
355
+ await writeFile(
356
+ join(root, 'test/census-services.test.mjs'),
357
+ "import { test } from 'node:test';\n"
358
+ + "import assert from 'node:assert/strict';\n"
359
+ + "import { scanServices } from '../scripts/census-local/scan-services.mjs';\n"
360
+ + "test('service scanner recognizes payments', () => {\n"
361
+ + " assert.deepEqual(scanServices(), ['services/payments/src']);\n"
362
+ + "});\n",
363
+ );
364
+ await exec('git', ['add', 'services', 'scripts/census-local', 'test/census-services.test.mjs'], { cwd: root });
365
+ const withScanner = await readJson(profilePath);
366
+ withScanner.localScanners.push({
367
+ surface: 'services/payments/src',
368
+ module: 'scripts/census-local/scan-services.mjs',
369
+ export: 'scanServices',
370
+ test: 'test/census-services.test.mjs',
371
+ });
372
+ await writeFile(profilePath, `${JSON.stringify(withScanner)}\n`);
373
+
374
+ const { candidate } = await runConsumerCensus(root);
375
+ const activated = await readJson(activePath);
376
+ assert.equal(candidate.state, 'current');
377
+ assert.equal(activated.profileReport.localScanners[0].test, 'test/census-services.test.mjs');
378
+ assert.notEqual(await readFile(activePath, 'utf8'), previous);
379
+
380
+ const fingerprintBeforeOverride = fingerprintCensus(activated);
381
+ withScanner.overrides.push({ reason: 'display only', scope: 'this change' });
382
+ await writeFile(profilePath, `${JSON.stringify(withScanner)}\n`);
383
+ const raw = await scanCensus({ repoRoot: root, enabled: true, hasActive: true });
384
+ assert.deepEqual(fingerprintCensus(raw), fingerprintBeforeOverride);
385
+ } finally {
386
+ await rm(root, { recursive: true, force: true });
387
+ }
388
+ });
389
+
390
+ test('local scanner proof rejects absolute, escaping, and symlinked paths', async () => {
391
+ const root = await makeBrownfield();
392
+ const foreign = await mkdtemp(join(tmpdir(), 'awk-foreign-census-proof-'));
393
+ try {
394
+ await mkdir(join(root, '.census'), { recursive: true });
395
+ const profilePath = join(root, PROFILE_RELATIVE_PATH);
396
+ const profile = {
397
+ schemaVersion: 1,
398
+ enabled: true,
399
+ decisions: [],
400
+ localScanners: [],
401
+ overrides: [],
402
+ };
403
+ await writeFile(profilePath, `${JSON.stringify(profile)}\n`);
404
+ const { activePath } = await runConsumerCensus(root);
405
+ const previous = await readFile(activePath, 'utf8');
406
+ await mkdir(join(root, 'services/payments/src'), { recursive: true });
407
+ await writeFile(join(root, 'services/payments/src/index.mjs'), 'export const payments = true;\n');
408
+ await exec('git', ['add', 'services'], { cwd: root });
409
+
410
+ await writeFile(join(foreign, 'scanner.mjs'), "export const scanServices = () => ['services/payments/src'];\n");
411
+ await writeFile(
412
+ join(foreign, 'scanner.test.mjs'),
413
+ "import { test } from 'node:test'; test('foreign proof', () => {});\n",
414
+ );
415
+ const records = [
416
+ { module: join(foreign, 'scanner.mjs'), test: join(foreign, 'scanner.test.mjs') },
417
+ {
418
+ module: `../${basename(foreign)}/scanner.mjs`,
419
+ test: `../${basename(foreign)}/scanner.test.mjs`,
420
+ },
421
+ ];
422
+ await symlink(join(foreign, 'scanner.mjs'), join(root, 'scanner-link.mjs'));
423
+ await symlink(join(foreign, 'scanner.test.mjs'), join(root, 'scanner-link.test.mjs'));
424
+ records.push({ module: 'scanner-link.mjs', test: 'scanner-link.test.mjs' });
425
+
426
+ for (const record of records) {
427
+ profile.localScanners = [{
428
+ surface: 'services/payments/src',
429
+ export: 'scanServices',
430
+ ...record,
431
+ }];
432
+ await writeFile(profilePath, `${JSON.stringify(profile)}\n`);
433
+ await assert.rejects(runConsumerCensus(root), (error) => error.state === 'failed');
434
+ assert.equal(await readFile(activePath, 'utf8'), previous);
435
+ }
436
+ } finally {
437
+ await rm(root, { recursive: true, force: true });
438
+ await rm(foreign, { recursive: true, force: true });
439
+ }
440
+ });