@monte3l/groundwork 1.0.0-rc.2 → 1.0.0-rc.4

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.
Files changed (170) hide show
  1. package/README.md +13 -5
  2. package/bin/m3l-groundwork.mjs +36 -2
  3. package/dist/assets.d.ts +10 -0
  4. package/dist/assets.js +14 -0
  5. package/dist/baseline-stage.d.ts +173 -0
  6. package/dist/baseline-stage.js +215 -0
  7. package/dist/caps.d.ts +4 -1
  8. package/dist/caps.js +17 -3
  9. package/dist/conflicts.d.ts +23 -2
  10. package/dist/conflicts.js +89 -11
  11. package/dist/customize-paths.d.ts +92 -0
  12. package/dist/customize-paths.js +115 -0
  13. package/dist/emit.d.ts +50 -1
  14. package/dist/emit.js +121 -13
  15. package/dist/fatal.d.ts +70 -0
  16. package/dist/fatal.js +132 -0
  17. package/dist/format-error.d.ts +113 -0
  18. package/dist/format-error.js +553 -0
  19. package/dist/fs-guard.d.ts +142 -0
  20. package/dist/fs-guard.js +222 -0
  21. package/dist/git.js +10 -1
  22. package/dist/harness/conformance.js +2 -0
  23. package/dist/harness/frontmatter.js +2 -13
  24. package/dist/harness/grade.js +37 -8
  25. package/dist/harness/rules.d.ts +20 -3
  26. package/dist/harness/rules.js +108 -6
  27. package/dist/harness/types.d.ts +13 -0
  28. package/dist/harness/types.js +3 -0
  29. package/dist/inventory.d.ts +77 -3
  30. package/dist/inventory.js +103 -12
  31. package/dist/jsonc.d.ts +49 -2
  32. package/dist/jsonc.js +140 -7
  33. package/dist/main.d.ts +62 -3
  34. package/dist/main.js +574 -67
  35. package/dist/merge-json.d.ts +47 -4
  36. package/dist/merge-json.js +120 -8
  37. package/dist/mode.js +12 -2
  38. package/dist/pack-stage.d.ts +210 -0
  39. package/dist/pack-stage.js +287 -0
  40. package/dist/packs.d.ts +21 -13
  41. package/dist/packs.js +241 -28
  42. package/dist/palette.d.ts +23 -0
  43. package/dist/palette.js +22 -0
  44. package/dist/plugin.d.ts +122 -6
  45. package/dist/plugin.js +687 -47
  46. package/dist/report.d.ts +21 -2
  47. package/dist/report.js +93 -5
  48. package/dist/staging.d.ts +176 -0
  49. package/dist/staging.js +375 -0
  50. package/dist/survey/fs-walk.d.ts +34 -2
  51. package/dist/survey/fs-walk.js +70 -5
  52. package/dist/survey/internal/blocked-path.d.ts +27 -0
  53. package/dist/survey/internal/blocked-path.js +129 -0
  54. package/dist/survey/internal/package-json.d.ts +13 -0
  55. package/dist/survey/internal/package-json.js +37 -0
  56. package/dist/survey/internal/read-guard.d.ts +178 -0
  57. package/dist/survey/internal/read-guard.js +276 -0
  58. package/dist/survey/survey-docs.d.ts +17 -2
  59. package/dist/survey/survey-docs.js +61 -21
  60. package/dist/survey/survey-harness.d.ts +20 -2
  61. package/dist/survey/survey-harness.js +87 -46
  62. package/dist/survey/survey-shape.d.ts +17 -2
  63. package/dist/survey/survey-shape.js +60 -46
  64. package/dist/survey/survey-toolchain.d.ts +16 -1
  65. package/dist/survey/survey-toolchain.js +69 -66
  66. package/dist/survey/survey.js +6 -4
  67. package/dist/survey/types.d.ts +79 -0
  68. package/dist/survey/types.js +2 -6
  69. package/dist/term.d.ts +80 -0
  70. package/dist/term.js +145 -0
  71. package/dist/tokens.js +2 -0
  72. package/dist/toolchain/conformance.js +2 -0
  73. package/dist/toolchain/grade.js +26 -8
  74. package/dist/toolchain/rules.d.ts +13 -4
  75. package/dist/toolchain/rules.js +16 -0
  76. package/dist/toolchain/tsconfig-chain.d.ts +2 -0
  77. package/dist/toolchain/tsconfig-chain.js +32 -8
  78. package/dist/toolchain/types.d.ts +16 -4
  79. package/dist/toolchain/types.js +3 -0
  80. package/package.json +4 -3
  81. package/plugin/skills/customize/SKILL.md +292 -18
  82. package/plugin/src/domain-map.ts +39 -14
  83. package/plugin/src/index.ts +5 -1
  84. package/plugin/src/kind-facet-map.ts +25 -8
  85. package/plugin/src/pack-map.ts +147 -25
  86. package/plugin/src/plugin-map.ts +236 -0
  87. package/templates/core/.claude/agents/Explore.md +0 -1
  88. package/templates/core/.claude/agents/code-implementer.md +4 -4
  89. package/templates/core/.claude/agents/code-reviewer.md +4 -4
  90. package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
  91. package/templates/core/.claude/agents/test-author.md +7 -5
  92. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
  93. package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
  94. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
  95. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
  96. package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
  97. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
  98. package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
  99. package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
  100. package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
  101. package/templates/core/.claude/rules/agent-dispatch.md +7 -0
  102. package/templates/core/.claude/rules/tests.md +2 -2
  103. package/templates/core/.claude/settings.json +5 -0
  104. package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
  105. package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
  106. package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
  107. package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
  108. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
  109. package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
  110. package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
  111. package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
  112. package/templates/core/.github/dependabot.yml +18 -0
  113. package/templates/core/.github/workflows/ci.yml +15 -15
  114. package/templates/core/.github/workflows/dependency-review.yml +2 -2
  115. package/templates/core/.github/workflows/security-audit.yml +10 -4
  116. package/templates/core/.prettierignore +4 -0
  117. package/templates/core/CLAUDE.md +53 -3
  118. package/templates/core/README.md +30 -10
  119. package/templates/core/_gitignore +17 -0
  120. package/templates/core/bin/check-exports.mjs +11 -5
  121. package/templates/core/bin/lib/agent-roster.mjs +1 -1
  122. package/templates/core/bin/lib/frontmatter.mjs +4 -2
  123. package/templates/core/bin/lib/harness-rules.mjs +169 -20
  124. package/templates/core/bin/lib/protected-paths.mjs +93 -5
  125. package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
  126. package/templates/core/bin/lib/verify-steps.mjs +29 -11
  127. package/templates/core/bin/verify.mjs +12 -0
  128. package/templates/core/eslint.config.js +5 -0
  129. package/templates/core/package.json +7 -7
  130. package/templates/core/vitest.config.ts +8 -1
  131. package/templates/packs/README.md +44 -24
  132. package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
  133. package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
  134. package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
  135. package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
  136. package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
  137. package/templates/packs/github/pack.json +19 -0
  138. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
  139. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
  140. package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
  141. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
  142. package/templates/packs/harness-extras/pack.json +18 -13
  143. package/templates/packs/publishing/files/.changeset/README.md +25 -0
  144. package/templates/packs/publishing/files/.changeset/config.json +7 -0
  145. package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
  146. package/templates/packs/publishing/files/.github/workflows/release.yml +295 -0
  147. package/templates/packs/publishing/files/REUSE.toml +28 -0
  148. package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
  149. package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
  150. package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
  151. package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
  152. package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
  153. package/templates/packs/publishing/pack.json +41 -0
  154. package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
  155. package/templates/packs/quality/pack.json +29 -0
  156. package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
  157. package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
  158. package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
  159. package/templates/packs/supply-chain/pack.json +19 -0
  160. package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
  161. package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
  162. package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
  163. package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
  164. package/templates/packs/worktrees/files/.worktreeinclude +11 -0
  165. package/templates/packs/worktrees/pack.json +64 -0
  166. package/templates/packs/statusline/pack.json +0 -31
  167. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
  168. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
  169. /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
  170. /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
@@ -5,24 +5,37 @@
5
5
  * distilled from Anthropic's published guidance and only ever warn -- a
6
6
  * subjective rule must never block a push.
7
7
  */
8
+ /** How severe a finding is: `structural` fails the gate; `rubric` only ever warns. */
8
9
  export type RuleLevel = "structural" | "rubric";
10
+ /** Which area of the harness (settings, hooks, skills, agents, rules, claude-md) a finding is about. */
9
11
  export type HarnessCategory = "settings" | "hooks" | "skills" | "agents" | "rules" | "claude-md";
12
+ /** Every {@link HarnessCategory}, as an ordered list -- so rubric tallies are iterated in a fixed order. */
10
13
  export declare const HARNESS_CATEGORIES: readonly HarnessCategory[];
14
+ /** One rule's result against one subject. */
11
15
  export interface HarnessFinding {
16
+ /** The id of the rule this finding came from. */
12
17
  ruleId: string;
18
+ /** Whether this finding fails the gate (`structural`) or only warns (`rubric`). */
13
19
  level: RuleLevel;
20
+ /** Which harness area this finding is about. */
14
21
  category: HarnessCategory;
15
22
  /** The file, skill, agent, or registration the finding is about. */
16
23
  subject: string;
24
+ /** Human-readable description of what the rule found. */
17
25
  message: string;
18
26
  }
19
27
  /** How many subjects a set of rules examined, and how many of those failed. */
20
28
  export interface CheckTally {
29
+ /** How many subjects the rules examined. */
21
30
  checked: number;
31
+ /** How many of the examined subjects failed. */
22
32
  failed: number;
23
33
  }
34
+ /** The full result of grading one project's harness. */
24
35
  export interface HarnessGrade {
36
+ /** Every finding from every rule, in rule order. */
25
37
  findings: HarnessFinding[];
38
+ /** The tally of structural checks: how many were checked and how many failed. */
26
39
  structural: CheckTally;
27
40
  /** Rubric tallies per category -- the absolute-quality measurement. */
28
41
  rubric: Record<HarnessCategory, CheckTally>;
@@ -1,3 +1,6 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+ /** Every {@link HarnessCategory}, as an ordered list -- so rubric tallies are iterated in a fixed order. */
1
4
  export const HARNESS_CATEGORIES = [
2
5
  "settings",
3
6
  "hooks",
@@ -1,13 +1,36 @@
1
+ import type { StagedBaselineFile } from "./baseline-stage.js";
1
2
  import type { CapCounts } from "./caps.js";
2
3
  import type { FileConflict } from "./conflicts.js";
3
4
  import type { HarnessConformance } from "./harness/conformance.js";
4
5
  import type { HarnessGrade } from "./harness/types.js";
5
6
  import type { ModeDetection } from "./mode.js";
7
+ import type { StagedPack } from "./pack-stage.js";
6
8
  import type { PackWiring } from "./packs.js";
7
9
  import type { ProjectSurvey } from "./survey/survey.js";
8
10
  import type { ToolchainConformance } from "./toolchain/conformance.js";
9
11
  import type { ToolchainGrade } from "./toolchain/types.js";
10
- export declare const INVENTORY_SCHEMA_VERSION = 4;
12
+ export declare const INVENTORY_SCHEMA_VERSION = 5;
13
+ /**
14
+ * Where adopt mode staged the baseline files the project lacks entirely
15
+ * (`baseline-stage.ts`), and how each staged copy is named.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const stagedBaseline: StagedBaseline = {
20
+ * dir: ".groundwork/baseline",
21
+ * suffix: ".staged",
22
+ * files: [{ path: "eslint.config.js", staged: "eslint.config.js.staged", sha256: "…" }],
23
+ * };
24
+ * ```
25
+ */
26
+ export interface StagedBaseline {
27
+ /** The staging directory, relative to the project root (e.g. `.groundwork/baseline`). */
28
+ dir: string;
29
+ /** The suffix every staged file name carries (`.staged`), so no toolchain glob ever matches one. */
30
+ suffix: string;
31
+ /** The staged files: install path, staged name relative to `dir`, and sha256 of the staged bytes. */
32
+ files: StagedBaselineFile[];
33
+ }
11
34
  export interface PackSurvey {
12
35
  name: string;
13
36
  modes: string[];
@@ -25,10 +48,15 @@ export interface Inventory {
25
48
  cliVersion: string;
26
49
  generatedAt: string;
27
50
  modeSignal: string;
51
+ /** The template tree's absolute path, in the platform's native form (not normalized). */
28
52
  templateRoot: string;
53
+ /** The adopted project's absolute path, in the platform's native form (not normalized). */
29
54
  targetDir: string;
55
+ /** The project survey; its paths are native, not normalized. */
30
56
  survey: ProjectSurvey;
57
+ /** Baseline-vs-project file collisions; every `relPath` uses `/` on every platform (normalized by `buildInventory`). */
31
58
  conflicts: FileConflict[];
59
+ /** Per-pack surveys; every `fileConflicts[].relPath` uses `/` on every platform, like `conflicts`. */
32
60
  packs: PackSurvey[];
33
61
  /** Wiring integrity and rubric quality of the project's existing harness. Absent when schemaVersion is below 3. */
34
62
  harnessGrade: HarnessGrade;
@@ -38,6 +66,10 @@ export interface Inventory {
38
66
  toolchainGrade: ToolchainGrade;
39
67
  /** How far the project's toolchain files have drifted from the baseline's -- information, never a defect. Absent when schemaVersion is below 4. */
40
68
  toolchainConformance: ToolchainConformance;
69
+ /** The "absent" baseline files, copied verbatim as inert `<path>.staged` copies for `/customize` to install from. Absent when schemaVersion is below 5; `dir` is project-relative, and `dir` and every `files[].path`/`files[].staged` use `/` on every platform. */
70
+ stagedBaseline: StagedBaseline;
71
+ /** Every pack, staged as inert `.staged` copies (manifest included) under `.groundwork/packs/<name>/` for `/customize` to install from after confirmation; one entry per pack, `dir` project-relative, and `dir` and every `path`/`staged` use `/` on every platform. Absent from inventories written before pack staging became inert (see the module header). */
72
+ stagedPacks: StagedPack[];
41
73
  }
42
74
  /**
43
75
  * Resolves this CLI package's own declared version. `packageJsonPath`
@@ -55,9 +87,51 @@ export interface BuildInventoryParams {
55
87
  packs: PackSurvey[];
56
88
  harnessGrade: HarnessGrade;
57
89
  toolchainGrade: ToolchainGrade;
90
+ stagedBaseline: StagedBaseline;
91
+ stagedPacks: StagedPack[];
58
92
  }
59
- /** Builds the inventory object. Does not write anything -- see `writeInventory`. */
93
+ /**
94
+ * Builds the inventory object. Does not write anything -- see `writeInventory`.
95
+ *
96
+ * Exactly these paths use `/` on every platform: each `conflicts[].relPath`
97
+ * and each `packs[].fileConflicts[].relPath`, normalized here once with
98
+ * {@link toPosixPath}, plus `stagedBaseline.dir` (built by the caller as a
99
+ * `/`-joined literal) and each `stagedBaseline.files[].path`/`.staged`
100
+ * (already normalized by `stageBaselineAdditions` with the same
101
+ * {@link toPosixPath}, so the conflict and staged paths agree), and
102
+ * `stagedPacks` (passed through verbatim: `stagePacks` builds each `dir` as
103
+ * a `/`-joined literal and normalizes every `path`/`staged` the same way).
104
+ * The harness/toolchain conformance
105
+ * summaries are computed from the normalized conflict paths. Every other
106
+ * path -- `templateRoot`, `targetDir`, and every path inside `survey` -- is
107
+ * passed through in the platform's native form. The caller's `conflicts` and
108
+ * `packs` are never mutated; new objects are returned.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const inventory = buildInventory({ detection, templateRoot, targetDir, survey,
113
+ * conflicts, packs, harnessGrade, toolchainGrade, stagedBaseline, stagedPacks });
114
+ * inventory.conflicts[0]?.relPath; // "src/index.ts", even on Windows
115
+ * ```
116
+ */
60
117
  export declare function buildInventory(params: BuildInventoryParams): Inventory;
61
- /** Writes `inventory.json` into `groundworkDir`, creating it if needed. */
118
+ /**
119
+ * Writes `inventory.json` into `groundworkDir`, creating it if needed.
120
+ *
121
+ * The write is atomic: the JSON goes to `inventory.json.tmp` first and is
122
+ * renamed over `inventory.json` only once complete, so a reader never sees a
123
+ * half-written file. On failure -- creating `groundworkDir` included -- the
124
+ * temp file is removed (best effort: a
125
+ * failed removal only warns, naming the temp file), any existing
126
+ * `inventory.json` is left untouched, and an `Error` is thrown, with the
127
+ * original failure as its `cause`, saying `.groundwork/` is incomplete and
128
+ * the CLI should be re-run.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const path = writeInventory(inventory, ".groundwork");
133
+ * // path: ".groundwork/inventory.json"
134
+ * ```
135
+ */
62
136
  export declare function writeInventory(inventory: Inventory, groundworkDir: string): string;
63
137
  //# sourceMappingURL=inventory.d.ts.map
package/dist/inventory.js CHANGED
@@ -1,20 +1,35 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
1
3
  /**
2
4
  * Serializes a survey + conflict plan + pack survey into
3
5
  * `.groundwork/inventory.json` -- the machine-readable handoff
4
- * `/customize`'s Step 0 reads instead of re-deriving the survey itself.
6
+ * `/customize`'s Step 0 (the adopt-mode reconcile step in `/customize`)
7
+ * reads instead of re-deriving the survey itself.
5
8
  * Schema-versioned so a future CLI release can tell an old inventory apart
6
9
  * from a current one; `/customize` must tolerate an older inventory
7
10
  * rather than crash on one -- `schemaVersion: 1` predates packs (no `packs`
8
11
  * field), `schemaVersion` below 3 predates the harness grade (no
9
12
  * `harnessGrade`/`harnessConformance`), and `schemaVersion` below 4 predates
10
- * the toolchain grade (no `toolchainGrade`/`toolchainConformance`).
13
+ * the toolchain grade (no `toolchainGrade`/`toolchainConformance`), and
14
+ * `schemaVersion` below 5 predates staged baseline additions (no
15
+ * `stagedBaseline`) -- `/customize` then falls back to reading absent files
16
+ * from `templateRoot`. From schema 5 on, each staged file is an inert copy
17
+ * named `<path>` + `stagedBaseline.suffix` (`.staged`) and carries the sha256
18
+ * of its staged bytes, so `/customize` can verify a copy before installing it.
19
+ * Schema 5 also carries `stagedPacks`, added additively without a version
20
+ * bump: every pack staged the same inert way under `.groundwork/packs/<name>/`
21
+ * (`pack-stage.ts`), its manifest included. No published release has
22
+ * written schema 5 yet; an inventory without `stagedPacks` came only from an
23
+ * unreleased development build, which staged packs unsuffixed at the same
24
+ * location.
11
25
  */
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
26
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
13
27
  import { dirname, join } from "node:path";
14
28
  import { fileURLToPath } from "node:url";
29
+ import { toPosixPath } from "./assets.js";
15
30
  import { summarizeHarnessConformance } from "./harness/conformance.js";
16
31
  import { summarizeToolchainConformance } from "./toolchain/conformance.js";
17
- export const INVENTORY_SCHEMA_VERSION = 4;
32
+ export const INVENTORY_SCHEMA_VERSION = 5;
18
33
  /** Resolves this CLI package's own `package.json`, relative to this module's runtime location. */
19
34
  function defaultPackageJsonPath() {
20
35
  const here = dirname(fileURLToPath(import.meta.url));
@@ -38,8 +53,44 @@ export function resolveCliVersion(packageJsonPath = defaultPackageJsonPath()) {
38
53
  return "unknown";
39
54
  }
40
55
  }
41
- /** Builds the inventory object. Does not write anything -- see `writeInventory`. */
56
+ /**
57
+ * Returns new `FileConflict` objects whose `relPath` uses `/`, leaving the
58
+ * caller's array and objects untouched (main.ts keeps the native form for
59
+ * real file operations).
60
+ */
61
+ function toPosixConflicts(conflicts) {
62
+ return conflicts.map((c) => ({ ...c, relPath: toPosixPath(c.relPath) }));
63
+ }
64
+ /**
65
+ * Builds the inventory object. Does not write anything -- see `writeInventory`.
66
+ *
67
+ * Exactly these paths use `/` on every platform: each `conflicts[].relPath`
68
+ * and each `packs[].fileConflicts[].relPath`, normalized here once with
69
+ * {@link toPosixPath}, plus `stagedBaseline.dir` (built by the caller as a
70
+ * `/`-joined literal) and each `stagedBaseline.files[].path`/`.staged`
71
+ * (already normalized by `stageBaselineAdditions` with the same
72
+ * {@link toPosixPath}, so the conflict and staged paths agree), and
73
+ * `stagedPacks` (passed through verbatim: `stagePacks` builds each `dir` as
74
+ * a `/`-joined literal and normalizes every `path`/`staged` the same way).
75
+ * The harness/toolchain conformance
76
+ * summaries are computed from the normalized conflict paths. Every other
77
+ * path -- `templateRoot`, `targetDir`, and every path inside `survey` -- is
78
+ * passed through in the platform's native form. The caller's `conflicts` and
79
+ * `packs` are never mutated; new objects are returned.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const inventory = buildInventory({ detection, templateRoot, targetDir, survey,
84
+ * conflicts, packs, harnessGrade, toolchainGrade, stagedBaseline, stagedPacks });
85
+ * inventory.conflicts[0]?.relPath; // "src/index.ts", even on Windows
86
+ * ```
87
+ */
42
88
  export function buildInventory(params) {
89
+ const conflicts = toPosixConflicts(params.conflicts);
90
+ const packs = params.packs.map((pack) => ({
91
+ ...pack,
92
+ fileConflicts: toPosixConflicts(pack.fileConflicts),
93
+ }));
43
94
  return {
44
95
  schemaVersion: INVENTORY_SCHEMA_VERSION,
45
96
  cliVersion: resolveCliVersion(),
@@ -48,19 +99,59 @@ export function buildInventory(params) {
48
99
  templateRoot: params.templateRoot,
49
100
  targetDir: params.targetDir,
50
101
  survey: params.survey,
51
- conflicts: params.conflicts,
52
- packs: params.packs,
102
+ conflicts,
103
+ packs,
53
104
  harnessGrade: params.harnessGrade,
54
- harnessConformance: summarizeHarnessConformance(params.conflicts),
105
+ harnessConformance: summarizeHarnessConformance(conflicts),
55
106
  toolchainGrade: params.toolchainGrade,
56
- toolchainConformance: summarizeToolchainConformance(params.conflicts),
107
+ toolchainConformance: summarizeToolchainConformance(conflicts),
108
+ stagedBaseline: params.stagedBaseline,
109
+ stagedPacks: params.stagedPacks,
57
110
  };
58
111
  }
59
- /** Writes `inventory.json` into `groundworkDir`, creating it if needed. */
112
+ /**
113
+ * Writes `inventory.json` into `groundworkDir`, creating it if needed.
114
+ *
115
+ * The write is atomic: the JSON goes to `inventory.json.tmp` first and is
116
+ * renamed over `inventory.json` only once complete, so a reader never sees a
117
+ * half-written file. On failure -- creating `groundworkDir` included -- the
118
+ * temp file is removed (best effort: a
119
+ * failed removal only warns, naming the temp file), any existing
120
+ * `inventory.json` is left untouched, and an `Error` is thrown, with the
121
+ * original failure as its `cause`, saying `.groundwork/` is incomplete and
122
+ * the CLI should be re-run.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const path = writeInventory(inventory, ".groundwork");
127
+ * // path: ".groundwork/inventory.json"
128
+ * ```
129
+ */
60
130
  export function writeInventory(inventory, groundworkDir) {
61
- mkdirSync(groundworkDir, { recursive: true });
62
131
  const path = join(groundworkDir, "inventory.json");
63
- writeFileSync(path, `${JSON.stringify(inventory, null, 2)}\n`);
132
+ const tmpPath = `${path}.tmp`;
133
+ try {
134
+ mkdirSync(groundworkDir, { recursive: true });
135
+ // Remove whatever already sits at the temp path (a crashed run's
136
+ // leftover, or a symlink planted there), then create it exclusively:
137
+ // "wx" fails rather than following a symlink raced in between.
138
+ rmSync(tmpPath, { force: true });
139
+ writeFileSync(tmpPath, `${JSON.stringify(inventory, null, 2)}\n`, {
140
+ flag: "wx",
141
+ });
142
+ renameSync(tmpPath, path);
143
+ }
144
+ catch (cause) {
145
+ try {
146
+ rmSync(tmpPath, { force: true });
147
+ }
148
+ catch (cleanupError) {
149
+ // Best effort: the write failure below is the error worth reporting,
150
+ // so a failed cleanup only warns, naming the leftover file.
151
+ console.warn(`warning: could not remove the temporary file ${tmpPath} -- delete it by hand (${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)})`);
152
+ }
153
+ throw new Error(`writing ${path} failed, so .groundwork/ is incomplete -- fix the cause and re-run the CLI`, { cause });
154
+ }
64
155
  return path;
65
156
  }
66
157
  //# sourceMappingURL=inventory.js.map
package/dist/jsonc.d.ts CHANGED
@@ -1,14 +1,61 @@
1
+ /**
2
+ * The outcome of reading/parsing JSONC. A failure says which step failed:
3
+ * `"read"` (the file is missing, unreadable, unresolvable or not a regular
4
+ * file) or `"parse"` (its text is not JSONC) -- so a caller never reports an
5
+ * unreadable file as unparseable.
6
+ */
1
7
  export type JsoncReadResult = {
2
8
  ok: true;
3
9
  value: unknown;
4
10
  } | {
5
11
  ok: false;
12
+ stage: "read" | "parse";
6
13
  error: string;
7
14
  };
8
- /** Strips `//` and block comments and trailing commas from JSONC source. */
15
+ /**
16
+ * Strips `//` and block comments and trailing commas from JSONC source. A
17
+ * trailing comma is removed in the same pass that tracks string literals, so
18
+ * a `,}` or `,]` inside a string (value or key) is never touched.
19
+ */
9
20
  export declare function stripJsoncNoise(content: string): string;
21
+ /**
22
+ * Strips `//` and block comments from JavaScript/TypeScript source, leaving
23
+ * single-quoted, double-quoted and template-literal strings alone. Unlike {@link stripJsoncNoise}
24
+ * it keeps trailing commas -- they are valid JS syntax, and removing them is
25
+ * a JSON-only cleanup.
26
+ *
27
+ * A character scanner, not a parser: a regex literal containing a quote
28
+ * (`/["']/`) or `//` can be misread as opening a string or a comment, and a
29
+ * `${...}` expression inside a template literal containing its own backtick
30
+ * or quote can close the outer template early. Neither shape appears in this
31
+ * project's actual `eslint.config.js`/`vitest.config.ts`/`verify-steps.mjs`
32
+ * files.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { stripJsComments } from "./jsonc.js";
37
+ * stripJsComments("const g = ['**' + '/*.js']; // note");
38
+ * // => "const g = ['**' + '/*.js']; "
39
+ * ```
40
+ */
41
+ export declare function stripJsComments(content: string): string;
10
42
  /** Parses JSONC source text, returning a structured result rather than throwing. */
11
43
  export declare function parseJsonc(content: string): JsoncReadResult;
12
- /** Reads and parses a JSONC file. A missing file is reported, not thrown. */
44
+ /**
45
+ * Reads and parses a JSONC file. A missing (`ENOENT`/`ENOTDIR`), unreadable
46
+ * (`EACCES`/`EPERM`), unresolvable (a symlink loop's `ELOOP`), not a regular
47
+ * file (a directory's `EISDIR`) or unparseable file is reported, not thrown
48
+ * -- each is a property of the file. The existence check is a real `stat`, never `existsSync`, so a file
49
+ * under a directory this process cannot search is reported with its errno
50
+ * rather than as absent. Any other failure, on the check or the read
51
+ * (`EIO`, `EMFILE`, ...), throws an `Error` naming the path, with the
52
+ * original failure as `cause`.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const read = readJsoncFile("/path/to/project/tsconfig.json");
57
+ * if (!read.ok) console.log(read.error);
58
+ * ```
59
+ */
13
60
  export declare function readJsoncFile(path: string): JsoncReadResult;
14
61
  //# sourceMappingURL=jsonc.d.ts.map
package/dist/jsonc.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
1
3
  /**
2
4
  * A tolerant reader for JSON-with-comments (JSONC) -- `tsconfig.json` and
3
5
  * friends use `//`/`/* *\/` comments and trailing commas that `JSON.parse`
@@ -6,13 +8,23 @@
6
8
  * to `JSON.parse`. It is not a full JSON5 parser -- no unquoted keys, no
7
9
  * single-quoted strings -- tsconfig-shaped input is the only intended use.
8
10
  */
9
- import { existsSync, readFileSync } from "node:fs";
10
- /** Strips `//` and block comments and trailing commas from JSONC source. */
11
+ import { readFileSync, statSync } from "node:fs";
12
+ import { errnoCode, isAbsentError, readFailure, unresolvableCode, } from "./survey/internal/read-guard.js";
13
+ /** Matches exactly the whitespace class `\s` covers, so a trailing comma is recognized across the same gaps as before. */
14
+ const WHITESPACE = /\s/;
15
+ /**
16
+ * Strips `//` and block comments and trailing commas from JSONC source. A
17
+ * trailing comma is removed in the same pass that tracks string literals, so
18
+ * a `,}` or `,]` inside a string (value or key) is never touched.
19
+ */
11
20
  export function stripJsoncNoise(content) {
12
21
  let result = "";
13
22
  let inString = false;
14
23
  let inLineComment = false;
15
24
  let inBlockComment = false;
25
+ // Index in `result` of the last comma emitted outside a string with only
26
+ // whitespace (or stripped comments) after it; -1 when there is none.
27
+ let pendingComma = -1;
16
28
  for (let i = 0; i < content.length; i++) {
17
29
  const ch = content[i];
18
30
  const next = content[i + 1];
@@ -44,6 +56,89 @@ export function stripJsoncNoise(content) {
44
56
  }
45
57
  if (ch === '"') {
46
58
  inString = true;
59
+ pendingComma = -1;
60
+ result += ch;
61
+ continue;
62
+ }
63
+ if (ch === "/" && next === "/") {
64
+ inLineComment = true;
65
+ i++;
66
+ continue;
67
+ }
68
+ if (ch === "/" && next === "*") {
69
+ inBlockComment = true;
70
+ i++;
71
+ continue;
72
+ }
73
+ if ((ch === "}" || ch === "]") && pendingComma !== -1) {
74
+ result = result.slice(0, pendingComma) + result.slice(pendingComma + 1);
75
+ }
76
+ if (ch === ",") {
77
+ pendingComma = result.length;
78
+ }
79
+ else if (ch !== undefined && !WHITESPACE.test(ch)) {
80
+ pendingComma = -1;
81
+ }
82
+ result += ch;
83
+ }
84
+ return result;
85
+ }
86
+ /**
87
+ * Strips `//` and block comments from JavaScript/TypeScript source, leaving
88
+ * single-quoted, double-quoted and template-literal strings alone. Unlike {@link stripJsoncNoise}
89
+ * it keeps trailing commas -- they are valid JS syntax, and removing them is
90
+ * a JSON-only cleanup.
91
+ *
92
+ * A character scanner, not a parser: a regex literal containing a quote
93
+ * (`/["']/`) or `//` can be misread as opening a string or a comment, and a
94
+ * `${...}` expression inside a template literal containing its own backtick
95
+ * or quote can close the outer template early. Neither shape appears in this
96
+ * project's actual `eslint.config.js`/`vitest.config.ts`/`verify-steps.mjs`
97
+ * files.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { stripJsComments } from "./jsonc.js";
102
+ * stripJsComments("const g = ['**' + '/*.js']; // note");
103
+ * // => "const g = ['**' + '/*.js']; "
104
+ * ```
105
+ */
106
+ export function stripJsComments(content) {
107
+ let result = "";
108
+ let quote;
109
+ let inLineComment = false;
110
+ let inBlockComment = false;
111
+ for (let i = 0; i < content.length; i++) {
112
+ const ch = content[i];
113
+ const next = content[i + 1];
114
+ if (inLineComment) {
115
+ if (ch === "\n") {
116
+ inLineComment = false;
117
+ result += ch;
118
+ }
119
+ continue;
120
+ }
121
+ if (inBlockComment) {
122
+ if (ch === "*" && next === "/") {
123
+ inBlockComment = false;
124
+ i++;
125
+ }
126
+ continue;
127
+ }
128
+ if (quote !== undefined) {
129
+ result += ch;
130
+ if (ch === "\\") {
131
+ result += next ?? "";
132
+ i++;
133
+ continue;
134
+ }
135
+ if (ch === quote) {
136
+ quote = undefined;
137
+ }
138
+ continue;
139
+ }
140
+ if (ch === '"' || ch === "'" || ch === "`") {
141
+ quote = ch;
47
142
  result += ch;
48
143
  continue;
49
144
  }
@@ -59,7 +154,7 @@ export function stripJsoncNoise(content) {
59
154
  }
60
155
  result += ch;
61
156
  }
62
- return result.replace(/,(\s*[}\]])/g, "$1");
157
+ return result;
63
158
  }
64
159
  /** Parses JSONC source text, returning a structured result rather than throwing. */
65
160
  export function parseJsonc(content) {
@@ -69,15 +164,53 @@ export function parseJsonc(content) {
69
164
  catch (error) {
70
165
  return {
71
166
  ok: false,
167
+ stage: "parse",
72
168
  error: error instanceof Error ? error.message : String(error),
73
169
  };
74
170
  }
75
171
  }
76
- /** Reads and parses a JSONC file. A missing file is reported, not thrown. */
172
+ /**
173
+ * Reads and parses a JSONC file. A missing (`ENOENT`/`ENOTDIR`), unreadable
174
+ * (`EACCES`/`EPERM`), unresolvable (a symlink loop's `ELOOP`), not a regular
175
+ * file (a directory's `EISDIR`) or unparseable file is reported, not thrown
176
+ * -- each is a property of the file. The existence check is a real `stat`, never `existsSync`, so a file
177
+ * under a directory this process cannot search is reported with its errno
178
+ * rather than as absent. Any other failure, on the check or the read
179
+ * (`EIO`, `EMFILE`, ...), throws an `Error` naming the path, with the
180
+ * original failure as `cause`.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const read = readJsoncFile("/path/to/project/tsconfig.json");
185
+ * if (!read.ok) console.log(read.error);
186
+ * ```
187
+ */
77
188
  export function readJsoncFile(path) {
78
- if (!existsSync(path)) {
79
- return { ok: false, error: `${path} does not exist` };
189
+ let content;
190
+ try {
191
+ statSync(path);
192
+ content = readFileSync(path, "utf8");
193
+ }
194
+ catch (error) {
195
+ if (isAbsentError(error)) {
196
+ return { ok: false, stage: "read", error: `${path} does not exist` };
197
+ }
198
+ if (errnoCode(error) === "EISDIR") {
199
+ return {
200
+ ok: false,
201
+ stage: "read",
202
+ error: `${path} is not a regular file`,
203
+ };
204
+ }
205
+ const code = unresolvableCode(error);
206
+ if (code === undefined)
207
+ throw readFailure(path, error);
208
+ return {
209
+ ok: false,
210
+ stage: "read",
211
+ error: `${path} is unreadable (${code})`,
212
+ };
80
213
  }
81
- return parseJsonc(readFileSync(path, "utf8"));
214
+ return parseJsonc(content);
82
215
  }
83
216
  //# sourceMappingURL=jsonc.js.map
package/dist/main.d.ts CHANGED
@@ -30,10 +30,69 @@ export declare class CliUsageError extends Error {
30
30
  export declare function parseArgs(argv: string[]): CliOptions;
31
31
  /** Resolves `templates/core` for a source checkout or a published tarball -- see `assets.ts`. */
32
32
  export declare function templatesCoreDir(): string;
33
- /** The post-`--pack`-install caps summary line(s) printed to fresh-mode's console output. */
33
+ /**
34
+ * The result of {@link formatCapsSummary}: the rendered summary text plus an
35
+ * explicit over-cap flag, so a caller never has to string-match the text to
36
+ * decide how to present it.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const summary: CapsSummary = { text: "= 5 agents, ...", overCap: false };
41
+ * console.log(summary.overCap ? `warning: ${summary.text}` : summary.text);
42
+ * ```
43
+ */
44
+ export interface CapsSummary {
45
+ /** The summary line(s), newline-joined, exactly as printed. */
46
+ readonly text: string;
47
+ /** `true` when the baseline plus installed packs exceeds any cap in `CAP_LIMITS`. */
48
+ readonly overCap: boolean;
49
+ }
50
+ /**
51
+ * The post-`--pack`-install caps summary line(s) printed to fresh-mode's
52
+ * console output, plus whether any cap is exceeded.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const summary = formatCapsSummary(baselineCounts, [
57
+ * { name: "harness-extras", budget: packBudget },
58
+ * ]);
59
+ * console.log(summary.text);
60
+ * ```
61
+ */
34
62
  export declare function formatCapsSummary(baseline: CapCounts, installed: {
35
63
  name: string;
36
64
  budget: CapCounts;
37
- }[]): string;
38
- export declare function main(argv: string[]): void;
65
+ }[]): CapsSummary;
66
+ /**
67
+ * Adopt mode's write-scope invariant (docs/assurance-case.md's trust
68
+ * boundary around the adopted project): every path it writes resolves under
69
+ * `<targetDir>/.groundwork/` or the guarded `/customize` install at
70
+ * `<targetDir>/.claude/skills/customize/` -- never an existing project file.
71
+ * `paths` may be absolute or relative to `targetDir`. Containment is
72
+ * {@link isPathContained}'s, so a sibling that merely shares a root's name
73
+ * as a prefix (`.groundwork-evil/`) is rejected. Both roots are derived from
74
+ * `customize-paths.ts`'s segment constants, so they cannot drift from where
75
+ * the install actually writes. The check is lexical -- it never touches the
76
+ * filesystem, so it does not detect a symlinked directory component; that
77
+ * refusal lives in the writers themselves (`fs-guard.ts`).
78
+ *
79
+ * @throws `AssertionError` (from `node:assert/strict`) naming the first path
80
+ * that escapes both allowed roots.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { assertAdoptWriteScope } from "./main.js";
85
+ *
86
+ * assertAdoptWriteScope("/work/app", [".groundwork/inventory.json"]); // ok
87
+ * assertAdoptWriteScope("/work/app", ["/work/app/package.json"]); // throws
88
+ * ```
89
+ */
90
+ export declare function assertAdoptWriteScope(targetDir: string, paths: readonly string[]): void;
91
+ /**
92
+ * Runs the CLI. `platform` is injectable so fresh mode's Windows refusal
93
+ * (a runtime error, exit 1, raised before anything is written) is
94
+ * unit-testable on any OS; it defaults to `process.platform`. Adopt mode and
95
+ * `--help`/`--version`/`--list-packs` run on every platform.
96
+ */
97
+ export declare function main(argv: string[], platform?: NodeJS.Platform): void;
39
98
  //# sourceMappingURL=main.d.ts.map