@monte3l/groundwork 0.0.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.
- package/README.md +23 -0
- package/bin/m3l-groundwork.mjs +10 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +79 -0
- package/dist/caps.d.ts +25 -0
- package/dist/caps.js +69 -0
- package/dist/conflicts.d.ts +12 -0
- package/dist/conflicts.js +77 -0
- package/dist/emit.d.ts +7 -0
- package/dist/emit.js +42 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +9 -0
- package/dist/harness/conformance.d.ts +20 -0
- package/dist/harness/conformance.js +18 -0
- package/dist/harness/frontmatter.d.ts +38 -0
- package/dist/harness/frontmatter.js +204 -0
- package/dist/harness/grade.d.ts +4 -0
- package/dist/harness/grade.js +105 -0
- package/dist/harness/rules.d.ts +55 -0
- package/dist/harness/rules.js +580 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.js +9 -0
- package/dist/inventory.d.ts +63 -0
- package/dist/inventory.js +66 -0
- package/dist/jsonc.d.ts +14 -0
- package/dist/jsonc.js +83 -0
- package/dist/main.d.ts +24 -0
- package/dist/main.js +297 -0
- package/dist/merge-json.d.ts +74 -0
- package/dist/merge-json.js +135 -0
- package/dist/mode.d.ts +19 -0
- package/dist/mode.js +53 -0
- package/dist/packs.d.ts +61 -0
- package/dist/packs.js +186 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +79 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +323 -0
- package/dist/survey/fs-walk.d.ts +14 -0
- package/dist/survey/fs-walk.js +60 -0
- package/dist/survey/survey-docs.d.ts +4 -0
- package/dist/survey/survey-docs.js +69 -0
- package/dist/survey/survey-harness.d.ts +4 -0
- package/dist/survey/survey-harness.js +121 -0
- package/dist/survey/survey-shape.d.ts +4 -0
- package/dist/survey/survey-shape.js +182 -0
- package/dist/survey/survey-toolchain.d.ts +4 -0
- package/dist/survey/survey-toolchain.js +217 -0
- package/dist/survey/survey.d.ts +5 -0
- package/dist/survey/survey.js +21 -0
- package/dist/survey/types.d.ts +117 -0
- package/dist/survey/types.js +8 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.js +13 -0
- package/dist/toolchain/conformance.d.ts +20 -0
- package/dist/toolchain/conformance.js +30 -0
- package/dist/toolchain/grade.d.ts +4 -0
- package/dist/toolchain/grade.js +244 -0
- package/dist/toolchain/rules.d.ts +118 -0
- package/dist/toolchain/rules.js +706 -0
- package/dist/toolchain/tsconfig-chain.d.ts +36 -0
- package/dist/toolchain/tsconfig-chain.js +116 -0
- package/dist/toolchain/types.d.ts +27 -0
- package/dist/toolchain/types.js +9 -0
- package/package.json +59 -0
- package/plugin/skills/customize/SKILL.md +305 -0
- package/plugin/src/domain-map.ts +134 -0
- package/plugin/src/index.ts +4 -0
- package/plugin/src/kind-facet-map.ts +174 -0
- package/plugin/src/pack-map.ts +65 -0
- package/templates/core/.claude/agents/Explore.md +43 -0
- package/templates/core/.claude/agents/code-implementer.md +258 -0
- package/templates/core/.claude/agents/code-reviewer.md +163 -0
- package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
- package/templates/core/.claude/agents/test-author.md +211 -0
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
- package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
- package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
- package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
- package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
- package/templates/core/.claude/rules/agent-dispatch.md +121 -0
- package/templates/core/.claude/rules/refactoring.md +52 -0
- package/templates/core/.claude/rules/src.md +114 -0
- package/templates/core/.claude/rules/tests.md +129 -0
- package/templates/core/.claude/settings.json +111 -0
- package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
- package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
- package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
- package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
- package/templates/core/.github/workflows/ci.yml +123 -0
- package/templates/core/.github/workflows/dependency-review.yml +26 -0
- package/templates/core/.github/workflows/security-audit.yml +54 -0
- package/templates/core/.node-version +1 -0
- package/templates/core/.prettierignore +5 -0
- package/templates/core/.prettierrc.json +4 -0
- package/templates/core/CLAUDE.md +127 -0
- package/templates/core/README.md +24 -0
- package/templates/core/_gitignore +19 -0
- package/templates/core/_npmrc +1 -0
- package/templates/core/bin/check-exports.mjs +92 -0
- package/templates/core/bin/check-harness.mjs +27 -0
- package/templates/core/bin/check-node-version.mjs +51 -0
- package/templates/core/bin/check-toolchain.mjs +20 -0
- package/templates/core/bin/lib/agent-roster.mjs +8 -0
- package/templates/core/bin/lib/frontmatter.mjs +210 -0
- package/templates/core/bin/lib/harness-rules.mjs +916 -0
- package/templates/core/bin/lib/protected-paths.mjs +23 -0
- package/templates/core/bin/lib/report.mjs +56 -0
- package/templates/core/bin/lib/signed-range.mjs +178 -0
- package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
- package/templates/core/bin/lib/verify-steps.mjs +131 -0
- package/templates/core/bin/lib/verify-steps.packs.json +1 -0
- package/templates/core/bin/lint-commit.mjs +50 -0
- package/templates/core/bin/strip-claude-trailers.mjs +25 -0
- package/templates/core/bin/verify.mjs +64 -0
- package/templates/core/commitlint.config.js +11 -0
- package/templates/core/docs/research/harness-refresh.md +27 -0
- package/templates/core/docs/research/typescript-refresh.md +32 -0
- package/templates/core/eslint.config.js +105 -0
- package/templates/core/knip.json +6 -0
- package/templates/core/lefthook.yml +39 -0
- package/templates/core/package.json +58 -0
- package/templates/core/pnpm-workspace.yaml +13 -0
- package/templates/core/src/index.ts +12 -0
- package/templates/core/tests/index.test.ts +8 -0
- package/templates/core/tsconfig.base.json +36 -0
- package/templates/core/tsconfig.build.json +10 -0
- package/templates/core/tsconfig.json +11 -0
- package/templates/core/vitest.config.ts +32 -0
- package/templates/packs/README.md +81 -0
- package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
- package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
- package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
- package/templates/packs/harness-extras/pack.json +65 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
- package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
- package/templates/packs/statusline/pack.json +31 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects codebase-shape facts: package manager, monorepo tooling, module
|
|
3
|
+
* system, pinned versions, source/test layout, and the evidence (not the
|
|
4
|
+
* verdict) for what kind of project this is. `/customize`'s interview step
|
|
5
|
+
* does the inferring; this module only records what is literally on disk.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { walkBounded } from "./fs-walk.js";
|
|
10
|
+
const FRAMEWORK_DEP_NAMES = [
|
|
11
|
+
"react",
|
|
12
|
+
"vue",
|
|
13
|
+
"svelte",
|
|
14
|
+
"next",
|
|
15
|
+
"nuxt",
|
|
16
|
+
"@angular/core",
|
|
17
|
+
"express",
|
|
18
|
+
"fastify",
|
|
19
|
+
"koa",
|
|
20
|
+
"@nestjs/core",
|
|
21
|
+
"hono",
|
|
22
|
+
];
|
|
23
|
+
function readPackageJson(dir) {
|
|
24
|
+
const path = join(dir, "package.json");
|
|
25
|
+
if (!existsSync(path)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function detectPackageManager(dir) {
|
|
36
|
+
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
37
|
+
return "pnpm";
|
|
38
|
+
if (existsSync(join(dir, "yarn.lock")))
|
|
39
|
+
return "yarn";
|
|
40
|
+
if (existsSync(join(dir, "bun.lockb")))
|
|
41
|
+
return "bun";
|
|
42
|
+
if (existsSync(join(dir, "package-lock.json")))
|
|
43
|
+
return "npm";
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
function extractYamlStringListUnder(content, key) {
|
|
47
|
+
const lines = content.split("\n");
|
|
48
|
+
const keyIndex = lines.findIndex((line) => line.trim() === `${key}:`);
|
|
49
|
+
if (keyIndex === -1) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const items = [];
|
|
53
|
+
for (const line of lines.slice(keyIndex + 1)) {
|
|
54
|
+
const match = /^\s*-\s*["']?([^"'#]+)["']?\s*$/.exec(line);
|
|
55
|
+
if (match?.[1] === undefined) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
items.push(match[1].trim());
|
|
59
|
+
}
|
|
60
|
+
return items;
|
|
61
|
+
}
|
|
62
|
+
function detectMonorepo(dir, packageJson) {
|
|
63
|
+
const pnpmWorkspacePath = join(dir, "pnpm-workspace.yaml");
|
|
64
|
+
if (existsSync(pnpmWorkspacePath)) {
|
|
65
|
+
const content = readFileSync(pnpmWorkspacePath, "utf8");
|
|
66
|
+
return {
|
|
67
|
+
tool: "pnpm-workspaces",
|
|
68
|
+
globs: extractYamlStringListUnder(content, "packages"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (existsSync(join(dir, "turbo.json"))) {
|
|
72
|
+
return { tool: "turbo", globs: [] };
|
|
73
|
+
}
|
|
74
|
+
if (existsSync(join(dir, "nx.json"))) {
|
|
75
|
+
return { tool: "nx", globs: [] };
|
|
76
|
+
}
|
|
77
|
+
if (existsSync(join(dir, "lerna.json"))) {
|
|
78
|
+
return { tool: "lerna", globs: [] };
|
|
79
|
+
}
|
|
80
|
+
const workspaces = packageJson?.["workspaces"];
|
|
81
|
+
if (Array.isArray(workspaces)) {
|
|
82
|
+
return {
|
|
83
|
+
tool: "npm-workspaces",
|
|
84
|
+
globs: workspaces.filter((w) => typeof w === "string"),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { tool: "none", globs: [] };
|
|
88
|
+
}
|
|
89
|
+
function detectModuleType(packageJson) {
|
|
90
|
+
const type = packageJson?.["type"];
|
|
91
|
+
if (type === "module")
|
|
92
|
+
return "module";
|
|
93
|
+
if (type === "commonjs")
|
|
94
|
+
return "commonjs";
|
|
95
|
+
return "unspecified";
|
|
96
|
+
}
|
|
97
|
+
function detectTypescriptVersion(packageJson) {
|
|
98
|
+
const deps = packageJson?.["dependencies"];
|
|
99
|
+
const devDeps = packageJson?.["devDependencies"];
|
|
100
|
+
const fromDeps = typeof deps === "object" && deps !== null
|
|
101
|
+
? deps["typescript"]
|
|
102
|
+
: undefined;
|
|
103
|
+
const fromDevDeps = typeof devDeps === "object" && devDeps !== null
|
|
104
|
+
? devDeps["typescript"]
|
|
105
|
+
: undefined;
|
|
106
|
+
const value = fromDevDeps ?? fromDeps;
|
|
107
|
+
return typeof value === "string" ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
function detectNodeVersionPin(dir, packageJson) {
|
|
110
|
+
for (const [source, filename] of [
|
|
111
|
+
[".node-version", ".node-version"],
|
|
112
|
+
[".nvmrc", ".nvmrc"],
|
|
113
|
+
]) {
|
|
114
|
+
const path = join(dir, filename);
|
|
115
|
+
if (existsSync(path)) {
|
|
116
|
+
return { source, value: readFileSync(path, "utf8").trim() };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const engines = packageJson?.["engines"];
|
|
120
|
+
const nodeEngine = typeof engines === "object" && engines !== null
|
|
121
|
+
? engines["node"]
|
|
122
|
+
: undefined;
|
|
123
|
+
if (typeof nodeEngine === "string") {
|
|
124
|
+
return { source: "package.json#engines.node", value: nodeEngine };
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
function detectSourceLayout(dir) {
|
|
129
|
+
if (existsSync(join(dir, "src")))
|
|
130
|
+
return "src";
|
|
131
|
+
if (existsSync(join(dir, "lib")))
|
|
132
|
+
return "lib";
|
|
133
|
+
const rootEntries = readdirSync(dir).filter((name) => /\.(ts|tsx|js|mjs)$/.test(name));
|
|
134
|
+
if (rootEntries.length > 0)
|
|
135
|
+
return "root";
|
|
136
|
+
return "unknown";
|
|
137
|
+
}
|
|
138
|
+
function hasColocatedTests(dir) {
|
|
139
|
+
return walkBounded(dir, 2).some((entry) => !entry.isDirectory && /\.(test|spec)\.[jt]sx?$/.test(entry.relPath));
|
|
140
|
+
}
|
|
141
|
+
function detectTestPlacement(dir) {
|
|
142
|
+
if (existsSync(join(dir, "tests")) || existsSync(join(dir, "test"))) {
|
|
143
|
+
return "tests-dir";
|
|
144
|
+
}
|
|
145
|
+
if (hasColocatedTests(dir)) {
|
|
146
|
+
return "colocated";
|
|
147
|
+
}
|
|
148
|
+
return "unknown";
|
|
149
|
+
}
|
|
150
|
+
function collectKindEvidence(packageJson) {
|
|
151
|
+
const deps = packageJson?.["dependencies"];
|
|
152
|
+
const devDeps = packageJson?.["devDependencies"];
|
|
153
|
+
const allDepNames = new Set([
|
|
154
|
+
...(typeof deps === "object" && deps !== null ? Object.keys(deps) : []),
|
|
155
|
+
...(typeof devDeps === "object" && devDeps !== null
|
|
156
|
+
? Object.keys(devDeps)
|
|
157
|
+
: []),
|
|
158
|
+
]);
|
|
159
|
+
return {
|
|
160
|
+
hasExportsMap: packageJson?.["exports"] !== undefined,
|
|
161
|
+
hasBinField: packageJson?.["bin"] !== undefined,
|
|
162
|
+
hasMainField: packageJson?.["main"] !== undefined,
|
|
163
|
+
frameworkDeps: FRAMEWORK_DEP_NAMES.filter((name) => allDepNames.has(name)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Surveys codebase shape at `dir`. Offline, read-only, records evidence rather than verdicts. */
|
|
167
|
+
export function surveyShape(dir) {
|
|
168
|
+
const packageJson = readPackageJson(dir);
|
|
169
|
+
const monorepo = detectMonorepo(dir, packageJson);
|
|
170
|
+
return {
|
|
171
|
+
packageManager: detectPackageManager(dir),
|
|
172
|
+
monorepoTool: monorepo.tool,
|
|
173
|
+
workspaceGlobs: monorepo.globs,
|
|
174
|
+
moduleType: detectModuleType(packageJson),
|
|
175
|
+
typescriptVersion: detectTypescriptVersion(packageJson),
|
|
176
|
+
nodeVersionPin: detectNodeVersionPin(dir, packageJson),
|
|
177
|
+
sourceLayout: detectSourceLayout(dir),
|
|
178
|
+
testPlacement: detectTestPlacement(dir),
|
|
179
|
+
kindEvidence: collectKindEvidence(packageJson),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=survey-shape.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ToolchainSurvey } from "./types.js";
|
|
2
|
+
/** Surveys toolchain enforcement at `dir`. Appends anything it could not parse to `undetermined`. */
|
|
3
|
+
export declare function surveyToolchain(dir: string, undetermined: string[]): ToolchainSurvey;
|
|
4
|
+
//# sourceMappingURL=survey-toolchain.d.ts.map
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects toolchain-enforcement facts: the tsconfig `extends` chain and its
|
|
3
|
+
* effective strict-family flags, which eslint/test/formatter/git-hook tool
|
|
4
|
+
* is in play and its config file, workflow files, and the full `scripts`
|
|
5
|
+
* block. YAML-shaped config (git hooks, workflows) is indexed and excerpted
|
|
6
|
+
* here, never parsed -- see `needsReading` on each -- because this package
|
|
7
|
+
* carries no YAML dependency.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { STRICT_FLAGS } from "../toolchain/rules.js";
|
|
12
|
+
import { loadTsconfigChain } from "../toolchain/tsconfig-chain.js";
|
|
13
|
+
/** Every flag the toolchain grader judges, so `effectiveFlags` and the grade cannot disagree. */
|
|
14
|
+
const STRICT_FLAG_NAMES = [...STRICT_FLAGS, "allowUnreachableCode"];
|
|
15
|
+
const ESLINT_PLUGIN_PATTERN = /["']((?:eslint-plugin-|@typescript-eslint\/)[a-z0-9-]+)["']/gi;
|
|
16
|
+
function readPackageJson(dir) {
|
|
17
|
+
const path = join(dir, "package.json");
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function findTsconfigPath(dir) {
|
|
28
|
+
for (const name of ["tsconfig.json"]) {
|
|
29
|
+
const path = join(dir, name);
|
|
30
|
+
if (existsSync(path))
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Follows a tsconfig's `extends` chain with the resolver the toolchain grader
|
|
37
|
+
* shares, so the survey and the grade cannot disagree about a project's
|
|
38
|
+
* effective flags. `files` are absolute and child-first. A file that fails to
|
|
39
|
+
* parse, or a relative `extends` that points at nothing, is a parse failure; a
|
|
40
|
+
* bare package specifier that is not installed is only a note -- the flags it
|
|
41
|
+
* would contribute are simply absent.
|
|
42
|
+
*/
|
|
43
|
+
function surveyTsconfig(dir, undetermined) {
|
|
44
|
+
const entryPath = findTsconfigPath(dir);
|
|
45
|
+
if (entryPath === undefined) {
|
|
46
|
+
return { files: [], effectiveFlags: {}, parsed: false };
|
|
47
|
+
}
|
|
48
|
+
const chain = loadTsconfigChain(dir, "tsconfig.json");
|
|
49
|
+
let parsed = chain.parsed;
|
|
50
|
+
for (const file of chain.files) {
|
|
51
|
+
if (file.error !== undefined) {
|
|
52
|
+
undetermined.push(`could not parse ${file.abs}: ${file.error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const link of chain.links) {
|
|
56
|
+
if (link.resolved)
|
|
57
|
+
continue;
|
|
58
|
+
if (link.kind === "relative") {
|
|
59
|
+
parsed = false;
|
|
60
|
+
undetermined.push(`could not parse ${link.attempted}: ${link.attempted} does not exist`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
undetermined.push(`tsconfig extends "${link.specifier}" from ${link.from} could not be resolved (is it installed?) -- the flags it sets are not in the effective set`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const effectiveFlags = {};
|
|
67
|
+
for (const flag of STRICT_FLAG_NAMES) {
|
|
68
|
+
const value = chain.options[flag];
|
|
69
|
+
if (value !== undefined)
|
|
70
|
+
effectiveFlags[flag] = value;
|
|
71
|
+
}
|
|
72
|
+
return { files: chain.files.map((file) => file.abs), effectiveFlags, parsed };
|
|
73
|
+
}
|
|
74
|
+
function surveyEslint(dir) {
|
|
75
|
+
const flatCandidates = [
|
|
76
|
+
"eslint.config.js",
|
|
77
|
+
"eslint.config.mjs",
|
|
78
|
+
"eslint.config.ts",
|
|
79
|
+
];
|
|
80
|
+
const legacyCandidates = [
|
|
81
|
+
".eslintrc.js",
|
|
82
|
+
".eslintrc.cjs",
|
|
83
|
+
".eslintrc.json",
|
|
84
|
+
".eslintrc",
|
|
85
|
+
];
|
|
86
|
+
for (const name of flatCandidates) {
|
|
87
|
+
const path = join(dir, name);
|
|
88
|
+
if (existsSync(path)) {
|
|
89
|
+
const content = readFileSync(path, "utf8");
|
|
90
|
+
return {
|
|
91
|
+
configFile: name,
|
|
92
|
+
flat: true,
|
|
93
|
+
referencedPlugins: extractPluginNames(content),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const name of legacyCandidates) {
|
|
98
|
+
const path = join(dir, name);
|
|
99
|
+
if (existsSync(path)) {
|
|
100
|
+
const content = readFileSync(path, "utf8");
|
|
101
|
+
return {
|
|
102
|
+
configFile: name,
|
|
103
|
+
flat: false,
|
|
104
|
+
referencedPlugins: extractPluginNames(content),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { configFile: undefined, flat: false, referencedPlugins: [] };
|
|
109
|
+
}
|
|
110
|
+
function extractPluginNames(content) {
|
|
111
|
+
const names = new Set();
|
|
112
|
+
for (const match of content.matchAll(ESLINT_PLUGIN_PATTERN)) {
|
|
113
|
+
const name = match[1];
|
|
114
|
+
if (name !== undefined)
|
|
115
|
+
names.add(name);
|
|
116
|
+
}
|
|
117
|
+
return [...names];
|
|
118
|
+
}
|
|
119
|
+
function surveyTestRunner(dir, scripts) {
|
|
120
|
+
const candidates = [
|
|
121
|
+
["vitest.config.ts", "vitest"],
|
|
122
|
+
["vitest.config.js", "vitest"],
|
|
123
|
+
["jest.config.js", "jest"],
|
|
124
|
+
["jest.config.cjs", "jest"],
|
|
125
|
+
["jest.config.ts", "jest"],
|
|
126
|
+
["jest.config.json", "jest"],
|
|
127
|
+
[".mocharc.json", "mocha"],
|
|
128
|
+
[".mocharc.js", "mocha"],
|
|
129
|
+
];
|
|
130
|
+
for (const [name, tool] of candidates) {
|
|
131
|
+
if (existsSync(join(dir, name))) {
|
|
132
|
+
return { tool, configFile: name };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const testScript = scripts["test"] ?? "";
|
|
136
|
+
if (/node --test/.test(testScript)) {
|
|
137
|
+
return { tool: "node-test", configFile: undefined };
|
|
138
|
+
}
|
|
139
|
+
return { tool: "unknown", configFile: undefined };
|
|
140
|
+
}
|
|
141
|
+
function surveyFormatter(dir) {
|
|
142
|
+
const prettierCandidates = [
|
|
143
|
+
".prettierrc.json",
|
|
144
|
+
".prettierrc.js",
|
|
145
|
+
".prettierrc",
|
|
146
|
+
".prettierrc.yaml",
|
|
147
|
+
".prettierrc.yml",
|
|
148
|
+
];
|
|
149
|
+
for (const name of prettierCandidates) {
|
|
150
|
+
if (existsSync(join(dir, name))) {
|
|
151
|
+
return { tool: "prettier", configFile: name };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (existsSync(join(dir, "biome.json"))) {
|
|
155
|
+
return { tool: "biome", configFile: "biome.json" };
|
|
156
|
+
}
|
|
157
|
+
return { tool: "unknown", configFile: undefined };
|
|
158
|
+
}
|
|
159
|
+
function surveyGitHooks(dir, undetermined) {
|
|
160
|
+
if (existsSync(join(dir, "lefthook.yml")) ||
|
|
161
|
+
existsSync(join(dir, "lefthook.yaml"))) {
|
|
162
|
+
const configFile = existsSync(join(dir, "lefthook.yml"))
|
|
163
|
+
? "lefthook.yml"
|
|
164
|
+
: "lefthook.yaml";
|
|
165
|
+
undetermined.push(`${configFile} found -- its stage commands need reading, not parsing`);
|
|
166
|
+
return { manager: "lefthook", configFile, needsReading: true };
|
|
167
|
+
}
|
|
168
|
+
if (existsSync(join(dir, ".husky"))) {
|
|
169
|
+
undetermined.push(".husky/ found -- its hook scripts need reading, not parsing");
|
|
170
|
+
return { manager: "husky", configFile: ".husky", needsReading: true };
|
|
171
|
+
}
|
|
172
|
+
if (existsSync(join(dir, "simple-git-hooks.json"))) {
|
|
173
|
+
return {
|
|
174
|
+
manager: "simple-git-hooks",
|
|
175
|
+
configFile: "simple-git-hooks.json",
|
|
176
|
+
needsReading: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { manager: "none", configFile: undefined, needsReading: false };
|
|
180
|
+
}
|
|
181
|
+
function surveyWorkflows(dir, undetermined) {
|
|
182
|
+
const workflowsDir = join(dir, ".github", "workflows");
|
|
183
|
+
if (!existsSync(workflowsDir)) {
|
|
184
|
+
return { files: [], needsReading: false };
|
|
185
|
+
}
|
|
186
|
+
const files = readdirSync(workflowsDir).filter((name) => /\.ya?ml$/.test(name));
|
|
187
|
+
if (files.length > 0) {
|
|
188
|
+
undetermined.push(`${files.length} workflow file(s) under .github/workflows -- their job steps need reading, not parsing`);
|
|
189
|
+
}
|
|
190
|
+
return { files, needsReading: files.length > 0 };
|
|
191
|
+
}
|
|
192
|
+
function surveyScripts(packageJson) {
|
|
193
|
+
const scripts = packageJson?.["scripts"];
|
|
194
|
+
if (typeof scripts !== "object" || scripts === null)
|
|
195
|
+
return {};
|
|
196
|
+
const result = {};
|
|
197
|
+
for (const [key, value] of Object.entries(scripts)) {
|
|
198
|
+
if (typeof value === "string")
|
|
199
|
+
result[key] = value;
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/** Surveys toolchain enforcement at `dir`. Appends anything it could not parse to `undetermined`. */
|
|
204
|
+
export function surveyToolchain(dir, undetermined) {
|
|
205
|
+
const packageJson = readPackageJson(dir);
|
|
206
|
+
const scripts = surveyScripts(packageJson);
|
|
207
|
+
return {
|
|
208
|
+
tsconfig: surveyTsconfig(dir, undetermined),
|
|
209
|
+
eslint: surveyEslint(dir),
|
|
210
|
+
testRunner: surveyTestRunner(dir, scripts),
|
|
211
|
+
formatter: surveyFormatter(dir),
|
|
212
|
+
gitHooks: surveyGitHooks(dir, undetermined),
|
|
213
|
+
workflows: surveyWorkflows(dir, undetermined),
|
|
214
|
+
scripts,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=survey-toolchain.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProjectSurvey } from "./types.js";
|
|
2
|
+
export type { ProjectSurvey } from "./types.js";
|
|
3
|
+
/** Surveys `dir` end to end: shape, toolchain, harness, and docs. Offline, read-only. */
|
|
4
|
+
export declare function surveyProject(dir: string): ProjectSurvey;
|
|
5
|
+
//# sourceMappingURL=survey.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The survey aggregate: runs all four collectors against a target directory
|
|
3
|
+
* and returns the combined `ProjectSurvey`. See `types.ts` for the shape
|
|
4
|
+
* and each `survey-*.ts` module's header for what it collects and why.
|
|
5
|
+
*/
|
|
6
|
+
import { surveyDocs } from "./survey-docs.js";
|
|
7
|
+
import { surveyHarness } from "./survey-harness.js";
|
|
8
|
+
import { surveyShape } from "./survey-shape.js";
|
|
9
|
+
import { surveyToolchain } from "./survey-toolchain.js";
|
|
10
|
+
/** Surveys `dir` end to end: shape, toolchain, harness, and docs. Offline, read-only. */
|
|
11
|
+
export function surveyProject(dir) {
|
|
12
|
+
const undetermined = [];
|
|
13
|
+
return {
|
|
14
|
+
shape: surveyShape(dir),
|
|
15
|
+
toolchain: surveyToolchain(dir, undetermined),
|
|
16
|
+
harness: surveyHarness(dir),
|
|
17
|
+
docs: surveyDocs(dir),
|
|
18
|
+
undetermined,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=survey.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
*/
|
|
7
|
+
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun" | "unknown";
|
|
8
|
+
export type MonorepoTool = "pnpm-workspaces" | "turbo" | "nx" | "lerna" | "npm-workspaces" | "none";
|
|
9
|
+
export type ModuleType = "module" | "commonjs" | "unspecified";
|
|
10
|
+
export type SourceLayout = "src" | "lib" | "root" | "unknown";
|
|
11
|
+
export type TestPlacement = "tests-dir" | "colocated" | "unknown";
|
|
12
|
+
export interface NodeVersionPin {
|
|
13
|
+
source: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
export interface KindEvidence {
|
|
17
|
+
hasExportsMap: boolean;
|
|
18
|
+
hasBinField: boolean;
|
|
19
|
+
hasMainField: boolean;
|
|
20
|
+
frameworkDeps: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface ShapeSurvey {
|
|
23
|
+
packageManager: PackageManager;
|
|
24
|
+
monorepoTool: MonorepoTool;
|
|
25
|
+
workspaceGlobs: string[];
|
|
26
|
+
moduleType: ModuleType;
|
|
27
|
+
typescriptVersion: string | undefined;
|
|
28
|
+
nodeVersionPin: NodeVersionPin | undefined;
|
|
29
|
+
sourceLayout: SourceLayout;
|
|
30
|
+
testPlacement: TestPlacement;
|
|
31
|
+
kindEvidence: KindEvidence;
|
|
32
|
+
}
|
|
33
|
+
export interface TsconfigSurvey {
|
|
34
|
+
/** The extends chain, entry file first (child before parent), as absolute paths. */
|
|
35
|
+
files: string[];
|
|
36
|
+
/** The effective value of each strict-family flag this baseline cares about, after following `extends`. */
|
|
37
|
+
effectiveFlags: Record<string, unknown>;
|
|
38
|
+
/** False if any file in the chain failed to parse -- see `undetermined` on the parent survey. */
|
|
39
|
+
parsed: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface EslintSurvey {
|
|
42
|
+
configFile: string | undefined;
|
|
43
|
+
flat: boolean;
|
|
44
|
+
referencedPlugins: string[];
|
|
45
|
+
}
|
|
46
|
+
type TestRunnerTool = "vitest" | "jest" | "mocha" | "node-test" | "unknown";
|
|
47
|
+
export interface TestRunnerSurvey {
|
|
48
|
+
tool: TestRunnerTool;
|
|
49
|
+
configFile: string | undefined;
|
|
50
|
+
}
|
|
51
|
+
type FormatterTool = "prettier" | "biome" | "unknown";
|
|
52
|
+
export interface FormatterSurvey {
|
|
53
|
+
tool: FormatterTool;
|
|
54
|
+
configFile: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
type GitHookManager = "lefthook" | "husky" | "simple-git-hooks" | "none";
|
|
57
|
+
export interface GitHooksSurvey {
|
|
58
|
+
manager: GitHookManager;
|
|
59
|
+
configFile: string | undefined;
|
|
60
|
+
/** True whenever the manager's config is YAML/shell-script shaped -- this module indexes it, never parses it. */
|
|
61
|
+
needsReading: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface WorkflowsSurvey {
|
|
64
|
+
files: string[];
|
|
65
|
+
needsReading: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface ToolchainSurvey {
|
|
68
|
+
tsconfig: TsconfigSurvey;
|
|
69
|
+
eslint: EslintSurvey;
|
|
70
|
+
testRunner: TestRunnerSurvey;
|
|
71
|
+
formatter: FormatterSurvey;
|
|
72
|
+
gitHooks: GitHooksSurvey;
|
|
73
|
+
workflows: WorkflowsSurvey;
|
|
74
|
+
scripts: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
export interface HarnessAgent {
|
|
77
|
+
name: string;
|
|
78
|
+
model: string | undefined;
|
|
79
|
+
}
|
|
80
|
+
export interface HarnessSkill {
|
|
81
|
+
name: string;
|
|
82
|
+
description: string | undefined;
|
|
83
|
+
}
|
|
84
|
+
export interface HarnessRule {
|
|
85
|
+
name: string;
|
|
86
|
+
paths: string | undefined;
|
|
87
|
+
}
|
|
88
|
+
export interface HarnessSurvey {
|
|
89
|
+
present: boolean;
|
|
90
|
+
settingsFile: string | undefined;
|
|
91
|
+
agents: HarnessAgent[];
|
|
92
|
+
skills: HarnessSkill[];
|
|
93
|
+
hooks: string[];
|
|
94
|
+
rules: HarnessRule[];
|
|
95
|
+
commands: string[];
|
|
96
|
+
hasSettingsLocal: boolean;
|
|
97
|
+
hasClaudeMd: boolean;
|
|
98
|
+
claudeMdHeadings: string[];
|
|
99
|
+
}
|
|
100
|
+
export interface DocFile {
|
|
101
|
+
path: string;
|
|
102
|
+
sizeBytes: number;
|
|
103
|
+
headings: string[];
|
|
104
|
+
}
|
|
105
|
+
export interface DocsSurvey {
|
|
106
|
+
files: DocFile[];
|
|
107
|
+
}
|
|
108
|
+
export interface ProjectSurvey {
|
|
109
|
+
shape: ShapeSurvey;
|
|
110
|
+
toolchain: ToolchainSurvey;
|
|
111
|
+
harness: HarnessSurvey;
|
|
112
|
+
docs: DocsSurvey;
|
|
113
|
+
/** Things the survey attempted and could not parse or classify -- never silently dropped. */
|
|
114
|
+
undetermined: string[];
|
|
115
|
+
}
|
|
116
|
+
export {};
|
|
117
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token substitution: a fixed string-replace table, not a template engine --
|
|
3
|
+
* the CLI has zero runtime dependencies it can avoid, and a `__TOKEN__`
|
|
4
|
+
* literal replace is all `templates/core` needs.
|
|
5
|
+
*/
|
|
6
|
+
export type TokenTable = Record<string, string>;
|
|
7
|
+
/**
|
|
8
|
+
* Replaces every `__KEY__` occurrence in `content` with `tokens[KEY]`, for
|
|
9
|
+
* every key in `tokens`. Unmatched `__KEY__`-shaped text that isn't a known
|
|
10
|
+
* token is left untouched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyTokens(content: string, tokens: TokenTable): string;
|
|
13
|
+
//# sourceMappingURL=tokens.d.ts.map
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces every `__KEY__` occurrence in `content` with `tokens[KEY]`, for
|
|
3
|
+
* every key in `tokens`. Unmatched `__KEY__`-shaped text that isn't a known
|
|
4
|
+
* token is left untouched.
|
|
5
|
+
*/
|
|
6
|
+
export function applyTokens(content, tokens) {
|
|
7
|
+
let result = content;
|
|
8
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
9
|
+
result = result.split(`__${key}__`).join(value);
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far a project's toolchain files have drifted from `templates/core`'s --
|
|
3
|
+
* the second number the grader reports, kept separate from rubric quality
|
|
4
|
+
* because `/customize` deliberately rewrites the baseline. "Divergent" is
|
|
5
|
+
* therefore information, never a defect. Derived from the conflict plan
|
|
6
|
+
* `planConflicts` already computed, so no second diff is ever run.
|
|
7
|
+
*/
|
|
8
|
+
import type { FileConflict } from "../conflicts.js";
|
|
9
|
+
export interface ToolchainConformance {
|
|
10
|
+
identical: number;
|
|
11
|
+
divergent: number;
|
|
12
|
+
absent: number;
|
|
13
|
+
/** Baseline toolchain files the project has edited. */
|
|
14
|
+
divergentFiles: string[];
|
|
15
|
+
/** Baseline toolchain files the project lacks. */
|
|
16
|
+
absentFiles: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Counts the toolchain-scoped subset of a baseline conflict plan. */
|
|
19
|
+
export declare function summarizeToolchainConformance(conflicts: FileConflict[]): ToolchainConformance;
|
|
20
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const TOOLCHAIN_FILES = new Set([
|
|
2
|
+
"package.json",
|
|
3
|
+
".node-version",
|
|
4
|
+
".prettierrc.json",
|
|
5
|
+
"knip.json",
|
|
6
|
+
"lefthook.yml",
|
|
7
|
+
"vitest.config.ts",
|
|
8
|
+
]);
|
|
9
|
+
function isToolchainPath(relPath) {
|
|
10
|
+
return (TOOLCHAIN_FILES.has(relPath) ||
|
|
11
|
+
/^tsconfig(\..+)?\.json$/.test(relPath) ||
|
|
12
|
+
/^eslint\.config\./.test(relPath) ||
|
|
13
|
+
relPath.startsWith("bin/") ||
|
|
14
|
+
relPath.startsWith(".github/workflows/"));
|
|
15
|
+
}
|
|
16
|
+
/** Counts the toolchain-scoped subset of a baseline conflict plan. */
|
|
17
|
+
export function summarizeToolchainConformance(conflicts) {
|
|
18
|
+
const scoped = conflicts.filter((conflict) => isToolchainPath(conflict.relPath));
|
|
19
|
+
const files = (status) => scoped
|
|
20
|
+
.filter((conflict) => conflict.status === status)
|
|
21
|
+
.map((conflict) => conflict.relPath);
|
|
22
|
+
return {
|
|
23
|
+
identical: files("identical").length,
|
|
24
|
+
divergent: files("divergent").length,
|
|
25
|
+
absent: files("absent").length,
|
|
26
|
+
divergentFiles: files("divergent"),
|
|
27
|
+
absentFiles: files("absent"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=conformance.js.map
|