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

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 +538 -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 +19 -13
  41. package/dist/packs.js +231 -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 +35 -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 +293 -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 +35 -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
@@ -1,3 +1,5 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
1
3
  /**
2
4
  * The survey aggregate: runs all four collectors against a target directory
3
5
  * and returns the combined `ProjectSurvey`. See `types.ts` for the shape
@@ -11,11 +13,11 @@ import { surveyToolchain } from "./survey-toolchain.js";
11
13
  export function surveyProject(dir) {
12
14
  const undetermined = [];
13
15
  return {
14
- shape: surveyShape(dir),
16
+ shape: surveyShape(dir, undetermined),
15
17
  toolchain: surveyToolchain(dir, undetermined),
16
- harness: surveyHarness(dir),
17
- docs: surveyDocs(dir),
18
- undetermined,
18
+ harness: surveyHarness(dir, undetermined),
19
+ docs: surveyDocs(dir, undetermined),
20
+ undetermined: [...new Set(undetermined)],
19
21
  };
20
22
  }
21
23
  //# sourceMappingURL=survey.js.map
@@ -4,32 +4,56 @@
4
4
  * `ProjectSurvey`, and `conflicts.ts`/`report.ts` need to name these shapes
5
5
  * without importing a collector module just for its types.
6
6
  */
7
+ /** The package manager, inferred from which lockfile is present -- `unknown` when none is. */
7
8
  export type PackageManager = "npm" | "pnpm" | "yarn" | "bun" | "unknown";
9
+ /** The monorepo tooling, inferred from its marker file (or `package.json`'s `workspaces`) -- `none` when nothing is found. */
8
10
  export type MonorepoTool = "pnpm-workspaces" | "turbo" | "nx" | "lerna" | "npm-workspaces" | "none";
11
+ /** `package.json`'s `type` field as declared -- `unspecified` when it is absent or not one of the two known values. */
9
12
  export type ModuleType = "module" | "commonjs" | "unspecified";
13
+ /** Where source lives: a `src/` or `lib/` directory, loose source files at the root, or `unknown`. */
10
14
  export type SourceLayout = "src" | "lib" | "root" | "unknown";
15
+ /** Where tests live: a `tests/`/`test/` directory, `*.test`/`*.spec` files next to the source, or `unknown`. */
11
16
  export type TestPlacement = "tests-dir" | "colocated" | "unknown";
17
+ /** A Node.js version pin, and the file or field it was read from. */
12
18
  export interface NodeVersionPin {
19
+ /** Where the pin was found: `.node-version`, `.nvmrc`, or `package.json#engines.node`. */
13
20
  source: string;
21
+ /** The pin's value verbatim (trimmed), never normalized to a semver range. */
14
22
  value: string;
15
23
  }
24
+ /** Raw `package.json` facts that hint at what kind of project this is -- evidence only, never the verdict. */
16
25
  export interface KindEvidence {
26
+ /** Whether `package.json` declares an `exports` field. */
17
27
  hasExportsMap: boolean;
28
+ /** Whether `package.json` declares a `bin` field. */
18
29
  hasBinField: boolean;
30
+ /** Whether `package.json` declares a `main` field. */
19
31
  hasMainField: boolean;
32
+ /** Well-known framework packages found among `dependencies`/`devDependencies`. */
20
33
  frameworkDeps: string[];
21
34
  }
35
+ /** Codebase-shape facts: package manager, monorepo tooling, module system, pins, and layout. */
22
36
  export interface ShapeSurvey {
37
+ /** The package manager, from which lockfile is present. */
23
38
  packageManager: PackageManager;
39
+ /** The monorepo tool, from its marker file or `package.json`'s `workspaces`. */
24
40
  monorepoTool: MonorepoTool;
41
+ /** The workspace package globs the monorepo tool declares -- empty when it declares none or none were read. */
25
42
  workspaceGlobs: string[];
43
+ /** `package.json`'s declared module system. */
26
44
  moduleType: ModuleType;
45
+ /** The `typescript` version range from `devDependencies` (preferred) or `dependencies`, verbatim. */
27
46
  typescriptVersion: string | undefined;
47
+ /** The first Node.js version pin found, or `undefined` when there is none. */
28
48
  nodeVersionPin: NodeVersionPin | undefined;
49
+ /** Where the project's source files live. */
29
50
  sourceLayout: SourceLayout;
51
+ /** Where the project's test files live. */
30
52
  testPlacement: TestPlacement;
53
+ /** Evidence for what kind of project this is, left for `/customize` to interpret. */
31
54
  kindEvidence: KindEvidence;
32
55
  }
56
+ /** The project's tsconfig `extends` chain and the strict-family flags it resolves to. */
33
57
  export interface TsconfigSurvey {
34
58
  /** The extends chain, entry file first (child before parent), as absolute paths. */
35
59
  files: string[];
@@ -38,77 +62,132 @@ export interface TsconfigSurvey {
38
62
  /** False if any file in the chain failed to parse -- see `undetermined` on the parent survey. */
39
63
  parsed: boolean;
40
64
  }
65
+ /** The project's ESLint config file and what it references. */
41
66
  export interface EslintSurvey {
67
+ /** The config file found at the project root, or `undefined` when there is none. */
42
68
  configFile: string | undefined;
69
+ /** True for a flat config (`eslint.config.*`); false for a legacy `.eslintrc*` one or when there is no config. */
43
70
  flat: boolean;
71
+ /** Plugin names found in the config's source text -- scraped, never by executing it. */
44
72
  referencedPlugins: string[];
45
73
  }
46
74
  type TestRunnerTool = "vitest" | "jest" | "mocha" | "node-test" | "unknown";
75
+ /** The project's test runner, from its config file or `test` script. */
47
76
  export interface TestRunnerSurvey {
77
+ /** The runner detected -- `unknown` when neither a config file nor the `test` script names one. */
48
78
  tool: TestRunnerTool;
79
+ /** The runner's config file, or `undefined` when none was found. */
49
80
  configFile: string | undefined;
50
81
  }
51
82
  type FormatterTool = "prettier" | "biome" | "unknown";
83
+ /** The project's code formatter, from its config file. */
52
84
  export interface FormatterSurvey {
85
+ /** The formatter detected -- `unknown` when no known config file is present. */
53
86
  tool: FormatterTool;
87
+ /** The formatter's config file, or `undefined` when none was found. */
54
88
  configFile: string | undefined;
55
89
  }
56
90
  type GitHookManager = "lefthook" | "husky" | "simple-git-hooks" | "none";
91
+ /** The project's git-hook manager and where its config lives. */
57
92
  export interface GitHooksSurvey {
93
+ /** The hook manager detected from its config file or directory -- `none` when none is found. */
58
94
  manager: GitHookManager;
95
+ /** The manager's config file or directory, or `undefined` when there is no manager. */
59
96
  configFile: string | undefined;
60
97
  /** True whenever the manager's config is YAML/shell-script shaped -- this module indexes it, never parses it. */
61
98
  needsReading: boolean;
62
99
  }
100
+ /** The project's CI workflow files under `.github/workflows/`. */
63
101
  export interface WorkflowsSurvey {
102
+ /** Workflow file names (`.yml`/`.yaml`), relative to `.github/workflows/`. */
64
103
  files: string[];
104
+ /** True whenever any workflow file exists -- YAML is indexed, never parsed, so its steps need reading. */
65
105
  needsReading: boolean;
66
106
  }
107
+ /** Every toolchain enforcement mechanism the survey found in effect. */
67
108
  export interface ToolchainSurvey {
109
+ /** The tsconfig `extends` chain and its effective strict-family flags. */
68
110
  tsconfig: TsconfigSurvey;
111
+ /** The ESLint config, if any. */
69
112
  eslint: EslintSurvey;
113
+ /** The test runner, if one was detected. */
70
114
  testRunner: TestRunnerSurvey;
115
+ /** The code formatter, if one was detected. */
71
116
  formatter: FormatterSurvey;
117
+ /** The git-hook manager, if one was detected. */
72
118
  gitHooks: GitHooksSurvey;
119
+ /** The CI workflow files, if any. */
73
120
  workflows: WorkflowsSurvey;
121
+ /** `package.json`'s `scripts`, name to command, verbatim (string values only). */
74
122
  scripts: Record<string, string>;
75
123
  }
124
+ /** One agent file under `.claude/agents/`. */
76
125
  export interface HarnessAgent {
126
+ /** The agent's file name without its `.md` extension. */
77
127
  name: string;
128
+ /** The agent's frontmatter `model` field, or `undefined` when absent or empty. */
78
129
  model: string | undefined;
79
130
  }
131
+ /** One skill directory under `.claude/skills/` that has a `SKILL.md`. */
80
132
  export interface HarnessSkill {
133
+ /** The skill's frontmatter `name`, falling back to its directory name. */
81
134
  name: string;
135
+ /** The skill's frontmatter `description`, or `undefined` when absent or empty. */
82
136
  description: string | undefined;
83
137
  }
138
+ /** One rule file under `.claude/rules/` -- the survey's index entry, unrelated to the harness grader's rule type. */
84
139
  export interface HarnessRule {
140
+ /** The rule's file name without its `.md` extension. */
85
141
  name: string;
142
+ /** The rule's frontmatter `paths` scope as text (a list joined with commas), or `undefined` when absent or empty. */
86
143
  paths: string | undefined;
87
144
  }
145
+ /** The project's existing Claude Code harness: its `.claude/` directory plus `CLAUDE.md`. */
88
146
  export interface HarnessSurvey {
147
+ /** Whether a `.claude/` directory exists at all. */
89
148
  present: boolean;
149
+ /** `settings.json` when `.claude/settings.json` exists, otherwise `undefined`. */
90
150
  settingsFile: string | undefined;
151
+ /** Every agent found under `.claude/agents/`. */
91
152
  agents: HarnessAgent[];
153
+ /** Every skill found under `.claude/skills/`. */
92
154
  skills: HarnessSkill[];
155
+ /** Hook script file names (`.mjs`/`.js`) under `.claude/hooks/`. */
93
156
  hooks: string[];
157
+ /** Every rule file found under `.claude/rules/`. */
94
158
  rules: HarnessRule[];
159
+ /** Command file names (`.md`) under `.claude/commands/`. */
95
160
  commands: string[];
161
+ /** Whether a `.claude/settings.local.json` exists, which may shadow shared settings. */
96
162
  hasSettingsLocal: boolean;
163
+ /** Whether a root `CLAUDE.md` exists -- recorded even when `.claude/` does not. */
97
164
  hasClaudeMd: boolean;
165
+ /** `CLAUDE.md`'s level 1-3 headings in document order -- empty when there is no `CLAUDE.md`. */
98
166
  claudeMdHeadings: string[];
99
167
  }
168
+ /** One indexed human-facing doc: where it is and its outline, never its content. */
100
169
  export interface DocFile {
170
+ /** The doc's path on disk. */
101
171
  path: string;
172
+ /** The doc's size on disk, in bytes. */
102
173
  sizeBytes: number;
174
+ /** The doc's level 1-3 Markdown headings in document order. */
103
175
  headings: string[];
104
176
  }
177
+ /** The index of human-facing docs and guidelines the survey found. */
105
178
  export interface DocsSurvey {
179
+ /** Every indexed doc: root README/CONTRIBUTING/style guides, then named doc directories. */
106
180
  files: DocFile[];
107
181
  }
182
+ /** The aggregate output of all four survey collectors -- adopt mode's offline index of the project. */
108
183
  export interface ProjectSurvey {
184
+ /** Codebase-shape facts. */
109
185
  shape: ShapeSurvey;
186
+ /** Toolchain enforcement in effect. */
110
187
  toolchain: ToolchainSurvey;
188
+ /** The existing Claude Code harness. */
111
189
  harness: HarnessSurvey;
190
+ /** The human-facing docs index. */
112
191
  docs: DocsSurvey;
113
192
  /** Things the survey attempted and could not parse or classify -- never silently dropped. */
114
193
  undetermined: string[];
@@ -1,8 +1,4 @@
1
- /**
2
- * Shared type definitions for the four survey collectors and their
3
- * aggregate. Kept in one file since every collector's output composes into
4
- * `ProjectSurvey`, and `conflicts.ts`/`report.ts` need to name these shapes
5
- * without importing a collector module just for its types.
6
- */
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
7
3
  export {};
8
4
  //# sourceMappingURL=types.js.map
package/dist/term.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { type PaletteRoleColors } from "./palette.js";
2
+ /**
3
+ * A semantic color role the palette defines for both themes.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import type { PaletteRole } from "./term.js";
8
+ * const role: PaletteRole = "success";
9
+ * ```
10
+ */
11
+ export type PaletteRole = keyof PaletteRoleColors;
12
+ /**
13
+ * Decides whether `stream` should receive color escapes. `NO_COLOR` (any
14
+ * value, even empty) disables color outright; otherwise `FORCE_COLOR` set to
15
+ * anything but `"0"` enables it; otherwise color follows `stream.isTTY`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import process from "node:process";
20
+ * import { supportsColor } from "./term.js";
21
+ * if (supportsColor(process.stdout, process.env)) {
22
+ * // safe to emit escapes
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function supportsColor(stream: {
27
+ readonly isTTY?: boolean;
28
+ }, env: NodeJS.ProcessEnv): boolean;
29
+ /**
30
+ * Converts a `#rrggbb` color into a 24-bit SGR foreground escape.
31
+ *
32
+ * @throws RangeError if `hex` is not exactly `#` plus six hex digits.
33
+ * @example
34
+ * ```ts
35
+ * import { truecolorSgr } from "./term.js";
36
+ * truecolorSgr("#086d31"); // "\x1b[38;2;8;109;49m"
37
+ * ```
38
+ */
39
+ export declare function truecolorSgr(hex: string): string;
40
+ /**
41
+ * Converts a `#rrggbb` color into the SGR foreground escape of the nearest
42
+ * of the 16 standard ANSI colors (squared Euclidean RGB distance).
43
+ *
44
+ * @throws RangeError if `hex` is not exactly `#` plus six hex digits.
45
+ * @example
46
+ * ```ts
47
+ * import { nearest16Sgr } from "./term.js";
48
+ * nearest16Sgr("#cd0000"); // "\x1b[31m"
49
+ * ```
50
+ */
51
+ export declare function nearest16Sgr(hex: string): string;
52
+ /**
53
+ * Picks the palette theme from `COLORFGBG` (`"fg;bg"`): `"light"` when the
54
+ * last segment is `15` or `7` (a white/light-grey background), `"dark"`
55
+ * otherwise, including when the variable is absent or malformed.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { resolveThemeId } from "./term.js";
60
+ * resolveThemeId({ COLORFGBG: "0;15" }); // "light"
61
+ * ```
62
+ */
63
+ export declare function resolveThemeId(env: NodeJS.ProcessEnv): "light" | "dark";
64
+ /**
65
+ * Wraps `text` in the palette color for `role`, or returns it unchanged when
66
+ * {@link supportsColor} says `stream` should not receive color. Uses a 24-bit
67
+ * escape when `COLORTERM` is `truecolor`/`24bit`, the nearest ANSI-16 color
68
+ * otherwise.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import process from "node:process";
73
+ * import { paint } from "./term.js";
74
+ * console.log(paint(process.stdout, "success", "done"));
75
+ * ```
76
+ */
77
+ export declare function paint(stream: {
78
+ readonly isTTY?: boolean;
79
+ }, role: PaletteRole, text: string, env?: NodeJS.ProcessEnv): string;
80
+ //# sourceMappingURL=term.d.ts.map
package/dist/term.js ADDED
@@ -0,0 +1,145 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * Zero-dependency terminal coloring over the generated {@link PALETTE}
5
+ * (`./palette.ts`, built from the vendored m3l-design tokens). Every function
6
+ * here is pure: color support, theme and color depth are all decided from
7
+ * the stream and environment the caller passes in.
8
+ */
9
+ import process from "node:process";
10
+ import { PALETTE } from "./palette.js";
11
+ const HEX_COLOR = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
12
+ /** The 16 standard ANSI foreground colors, as xterm's default RGB values. */
13
+ const ANSI_16 = [
14
+ { code: 30, rgb: [0, 0, 0] },
15
+ { code: 31, rgb: [205, 0, 0] },
16
+ { code: 32, rgb: [0, 205, 0] },
17
+ { code: 33, rgb: [205, 205, 0] },
18
+ { code: 34, rgb: [0, 0, 238] },
19
+ { code: 35, rgb: [205, 0, 205] },
20
+ { code: 36, rgb: [0, 205, 205] },
21
+ { code: 37, rgb: [229, 229, 229] },
22
+ { code: 90, rgb: [127, 127, 127] },
23
+ { code: 91, rgb: [255, 0, 0] },
24
+ { code: 92, rgb: [0, 255, 0] },
25
+ { code: 93, rgb: [255, 255, 0] },
26
+ { code: 94, rgb: [92, 92, 255] },
27
+ { code: 95, rgb: [255, 0, 255] },
28
+ { code: 96, rgb: [0, 255, 255] },
29
+ { code: 97, rgb: [255, 255, 255] },
30
+ ];
31
+ const RESET = "\x1b[0m";
32
+ /** Parses a strict `#rrggbb` string; throws a `RangeError` naming the value otherwise. */
33
+ function parseHex(hex) {
34
+ const match = HEX_COLOR.exec(hex);
35
+ const [, r, g, b] = match ?? [];
36
+ if (r === undefined || g === undefined || b === undefined) {
37
+ throw new RangeError(`invalid hex color ${JSON.stringify(hex)}: expected #rrggbb`);
38
+ }
39
+ return [
40
+ Number.parseInt(r, 16),
41
+ Number.parseInt(g, 16),
42
+ Number.parseInt(b, 16),
43
+ ];
44
+ }
45
+ /**
46
+ * Decides whether `stream` should receive color escapes. `NO_COLOR` (any
47
+ * value, even empty) disables color outright; otherwise `FORCE_COLOR` set to
48
+ * anything but `"0"` enables it; otherwise color follows `stream.isTTY`.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import process from "node:process";
53
+ * import { supportsColor } from "./term.js";
54
+ * if (supportsColor(process.stdout, process.env)) {
55
+ * // safe to emit escapes
56
+ * }
57
+ * ```
58
+ */
59
+ export function supportsColor(stream, env) {
60
+ if (env["NO_COLOR"] !== undefined)
61
+ return false;
62
+ const forceColor = env["FORCE_COLOR"];
63
+ if (forceColor !== undefined)
64
+ return forceColor !== "0";
65
+ return stream.isTTY === true;
66
+ }
67
+ /**
68
+ * Converts a `#rrggbb` color into a 24-bit SGR foreground escape.
69
+ *
70
+ * @throws RangeError if `hex` is not exactly `#` plus six hex digits.
71
+ * @example
72
+ * ```ts
73
+ * import { truecolorSgr } from "./term.js";
74
+ * truecolorSgr("#086d31"); // "\x1b[38;2;8;109;49m"
75
+ * ```
76
+ */
77
+ export function truecolorSgr(hex) {
78
+ const [r, g, b] = parseHex(hex);
79
+ return `\x1b[38;2;${String(r)};${String(g)};${String(b)}m`;
80
+ }
81
+ /**
82
+ * Converts a `#rrggbb` color into the SGR foreground escape of the nearest
83
+ * of the 16 standard ANSI colors (squared Euclidean RGB distance).
84
+ *
85
+ * @throws RangeError if `hex` is not exactly `#` plus six hex digits.
86
+ * @example
87
+ * ```ts
88
+ * import { nearest16Sgr } from "./term.js";
89
+ * nearest16Sgr("#cd0000"); // "\x1b[31m"
90
+ * ```
91
+ */
92
+ export function nearest16Sgr(hex) {
93
+ const [r, g, b] = parseHex(hex);
94
+ let bestCode = 30;
95
+ let bestDistance = Number.POSITIVE_INFINITY;
96
+ for (const { code, rgb } of ANSI_16) {
97
+ const distance = (r - rgb[0]) ** 2 + (g - rgb[1]) ** 2 + (b - rgb[2]) ** 2;
98
+ if (distance < bestDistance) {
99
+ bestDistance = distance;
100
+ bestCode = code;
101
+ }
102
+ }
103
+ return `\x1b[${String(bestCode)}m`;
104
+ }
105
+ /**
106
+ * Picks the palette theme from `COLORFGBG` (`"fg;bg"`): `"light"` when the
107
+ * last segment is `15` or `7` (a white/light-grey background), `"dark"`
108
+ * otherwise, including when the variable is absent or malformed.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { resolveThemeId } from "./term.js";
113
+ * resolveThemeId({ COLORFGBG: "0;15" }); // "light"
114
+ * ```
115
+ */
116
+ export function resolveThemeId(env) {
117
+ const colorfgbg = env["COLORFGBG"];
118
+ if (colorfgbg?.includes(";") !== true)
119
+ return "dark";
120
+ const background = colorfgbg.slice(colorfgbg.lastIndexOf(";") + 1);
121
+ return background === "15" || background === "7" ? "light" : "dark";
122
+ }
123
+ /**
124
+ * Wraps `text` in the palette color for `role`, or returns it unchanged when
125
+ * {@link supportsColor} says `stream` should not receive color. Uses a 24-bit
126
+ * escape when `COLORTERM` is `truecolor`/`24bit`, the nearest ANSI-16 color
127
+ * otherwise.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * import process from "node:process";
132
+ * import { paint } from "./term.js";
133
+ * console.log(paint(process.stdout, "success", "done"));
134
+ * ```
135
+ */
136
+ export function paint(stream, role, text, env = process.env) {
137
+ if (!supportsColor(stream, env))
138
+ return text;
139
+ const hex = PALETTE[resolveThemeId(env)][role];
140
+ const colorterm = env["COLORTERM"];
141
+ const truecolor = colorterm === "truecolor" || colorterm === "24bit";
142
+ const sgr = truecolor ? truecolorSgr(hex) : nearest16Sgr(hex);
143
+ return `${sgr}${text}${RESET}`;
144
+ }
145
+ //# sourceMappingURL=term.js.map
package/dist/tokens.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
1
3
  /**
2
4
  * Replaces every `__KEY__` occurrence in `content` with `tokens[KEY]`, for
3
5
  * every key in `tokens`. Unmatched `__KEY__`-shaped text that isn't a known
@@ -1,3 +1,5 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
1
3
  const TOOLCHAIN_FILES = new Set([
2
4
  "package.json",
3
5
  ".node-version",
@@ -1,3 +1,5 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
1
3
  /**
2
4
  * Grades a project's TypeScript toolchain. `gradeToolchain` reads the tsconfig
3
5
  * chains, ESLint and vitest configs, `package.json`, and the verify-step
@@ -7,8 +9,8 @@
7
9
  */
8
10
  import { readFileSync } from "node:fs";
9
11
  import { join } from "node:path";
10
- import { readJsoncFile, stripJsoncNoise } from "../jsonc.js";
11
- import { walkBounded } from "../survey/fs-walk.js";
12
+ import { readJsoncFile, stripJsComments } from "../jsonc.js";
13
+ import { walkBoundedForGrading } from "../survey/fs-walk.js";
12
14
  import { RULES } from "./rules.js";
13
15
  import { loadTsconfigChain } from "./tsconfig-chain.js";
14
16
  import { TOOLCHAIN_CATEGORIES } from "./types.js";
@@ -45,7 +47,7 @@ function readRaw(path) {
45
47
  /** File text with comments stripped, or `undefined` when unreadable. */
46
48
  function readSource(path) {
47
49
  try {
48
- return stripJsoncNoise(readFileSync(path, "utf8"));
50
+ return stripJsComments(readFileSync(path, "utf8"));
49
51
  }
50
52
  catch {
51
53
  return undefined;
@@ -72,21 +74,37 @@ function scrapeGateSteps(text) {
72
74
  * Reads every `verify.mjs` invocation out of one YAML surface as data: the
73
75
  * `--group`/`--step` each names. YAML is scraped, never parsed. An invocation
74
76
  * naming neither (a matrix, or a bare full run) marks the surface `dynamic`.
77
+ * Comments are stripped per line before any `\` line continuation is joined,
78
+ * so a comment ending in `\` cannot swallow the next line. A quoted value is
79
+ * read unquoted. A mention inside an `echo` (in the same shell command, not an
80
+ * earlier `&&`/`;`/`|` segment) or inside a step's `name:` with no `run:`
81
+ * before it on the line is not an invocation.
75
82
  */
76
83
  function scrapeLaneInvocations(text) {
77
84
  const groups = [];
78
85
  const steps = [];
79
86
  let dynamic = false;
80
87
  let seen = false;
81
- for (const raw of text.split("\n")) {
82
- const line = raw.replace(/(^|\s)#.*$/, "");
88
+ const logicalLines = text
89
+ .split("\n")
90
+ .map((raw) => raw.replace(/(^|\s)#.*$/, ""))
91
+ .join("\n")
92
+ .replace(/\\\r?\n[ \t]*/g, " ")
93
+ .split("\n");
94
+ for (const line of logicalLines) {
83
95
  const at = line.indexOf("verify.mjs");
84
96
  if (at === -1)
85
97
  continue;
98
+ const prefix = line.slice(0, at);
99
+ const lastSegment = prefix.split(/&&|\|\||[;|]/).pop() ?? "";
100
+ if (/\becho\b/.test(lastSegment))
101
+ continue;
102
+ if (/\bname\s*:/.test(prefix) && !/\brun\s*:/.test(prefix))
103
+ continue;
86
104
  seen = true;
87
105
  const rest = line.slice(at);
88
- const group = /--group[ =]+([A-Za-z][\w-]*)/.exec(rest);
89
- const step = /--step[ =]+([A-Za-z][\w-]*)/.exec(rest);
106
+ const group = /--group[ =]+["']?([A-Za-z][\w-]*)["']?/.exec(rest);
107
+ const step = /--step[ =]+["']?([A-Za-z][\w-]*)["']?/.exec(rest);
90
108
  if (group?.[1] !== undefined)
91
109
  groups.push(group[1]);
92
110
  else if (step?.[1] !== undefined)
@@ -97,7 +115,7 @@ function scrapeLaneInvocations(text) {
97
115
  return { seen, groups, steps, dynamic };
98
116
  }
99
117
  function loadSnapshot(root) {
100
- const entries = walkBounded(root, PROJECT_WALK_DEPTH);
118
+ const entries = walkBoundedForGrading(root, PROJECT_WALK_DEPTH);
101
119
  const projectFiles = new Set(entries.filter((e) => !e.isDirectory).map((e) => e.relPath));
102
120
  const rootFiles = [...projectFiles].filter((p) => !p.includes("/"));
103
121
  const packageRead = readJsoncFile(join(root, "package.json"));
@@ -1,10 +1,19 @@
1
1
  /**
2
2
  * The toolchain grader's rules. Each rule is a pure function over a
3
3
  * `ToolchainSnapshot` read once by `grade.ts`, so none of them touches the
4
- * filesystem. Structural rules catch wiring defects `tsc` and ESLint do not --
5
- * a build project that emits nowhere, a verify step naming a script that does
6
- * not exist -- and fail a gate. Rubric rules encode the floor official
7
- * TypeScript / typescript-eslint guidance sets and only ever warn.
4
+ * filesystem. Rules come in two levels.
5
+ *
6
+ * Structural rules are pass/fail wiring checks: they catch defects `tsc` and
7
+ * ESLint do not -- a build project that emits nowhere, a verify step naming a
8
+ * script that does not exist -- and fail a gate.
9
+ *
10
+ * Rubric rules are a quality-judgement checklist rather than a wiring check:
11
+ * each scores how closely the project follows a recommended practice, and a
12
+ * miss only ever warns. What they encode is the floor -- the minimum baseline
13
+ * of practice that current official TypeScript / typescript-eslint guidance
14
+ * recommends (strict-family flags on, a modern module target, type-aware
15
+ * linting, and so on). A project can go beyond the floor; falling below it is
16
+ * what a rubric rule reports.
8
17
  *
9
18
  * `eslint.config.js`, `vitest.config.ts` and `verify-steps.mjs` are executable
10
19
  * JavaScript, so their rules are regex scrapes over comment-stripped source,