dw-sdlc 0.0.0-stage → 0.2.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -2
  3. package/config/agentic.config.schema.json +375 -0
  4. package/config/references.schema.json +56 -0
  5. package/dist/agentic-config.d.ts +167 -0
  6. package/dist/agentic-config.js +305 -0
  7. package/dist/catalogue.d.ts +36 -0
  8. package/dist/catalogue.js +74 -0
  9. package/dist/checks/agents-map.d.ts +7 -0
  10. package/dist/checks/agents-map.js +159 -0
  11. package/dist/checks/check.d.ts +27 -0
  12. package/dist/checks/check.js +21 -0
  13. package/dist/checks/comment-budget.d.ts +6 -0
  14. package/dist/checks/comment-budget.js +141 -0
  15. package/dist/checks/decision-records.d.ts +6 -0
  16. package/dist/checks/decision-records.js +105 -0
  17. package/dist/checks/file-size-ratchet.d.ts +10 -0
  18. package/dist/checks/file-size-ratchet.js +194 -0
  19. package/dist/checks/index.d.ts +12 -0
  20. package/dist/checks/index.js +42 -0
  21. package/dist/checks/markdown-budget.d.ts +15 -0
  22. package/dist/checks/markdown-budget.js +42 -0
  23. package/dist/checks/markdown-refs.d.ts +6 -0
  24. package/dist/checks/markdown-refs.js +73 -0
  25. package/dist/checks/primitives.d.ts +8 -0
  26. package/dist/checks/primitives.js +103 -0
  27. package/dist/checks/temporal-language.d.ts +6 -0
  28. package/dist/checks/temporal-language.js +58 -0
  29. package/dist/checks/tree.d.ts +8 -0
  30. package/dist/checks/tree.js +30 -0
  31. package/dist/cli.d.ts +2 -0
  32. package/dist/cli.js +43 -0
  33. package/dist/commands/config.d.ts +2 -0
  34. package/dist/commands/config.js +76 -0
  35. package/dist/commands/doctor.d.ts +57 -0
  36. package/dist/commands/doctor.js +341 -0
  37. package/dist/commands/gate.d.ts +2 -0
  38. package/dist/commands/gate.js +231 -0
  39. package/dist/commands/primitives.d.ts +2 -0
  40. package/dist/commands/primitives.js +106 -0
  41. package/dist/commands/references-setup.d.ts +2 -0
  42. package/dist/commands/references-setup.js +202 -0
  43. package/dist/commands/references-sync.d.ts +2 -0
  44. package/dist/commands/references-sync.js +341 -0
  45. package/dist/commands/setup.d.ts +30 -0
  46. package/dist/commands/setup.js +352 -0
  47. package/dist/json-schema.d.ts +8 -0
  48. package/dist/json-schema.js +226 -0
  49. package/dist/markdown.d.ts +31 -0
  50. package/dist/markdown.js +115 -0
  51. package/dist/pins.d.ts +16 -0
  52. package/dist/pins.js +82 -0
  53. package/dist/references-key.d.ts +31 -0
  54. package/dist/references-key.js +149 -0
  55. package/dist/report.d.ts +13 -0
  56. package/dist/report.js +29 -0
  57. package/dist/router-block.d.ts +13 -0
  58. package/dist/router-block.js +60 -0
  59. package/dist/setup-rules.d.ts +22 -0
  60. package/dist/setup-rules.js +98 -0
  61. package/package.json +30 -3
  62. package/templates/router.md +38 -0
@@ -0,0 +1,167 @@
1
+ export declare const COMMITTED_FILE = ".ai/agentic.config.json";
2
+ export declare const LOCAL_FILE = ".ai/agentic.config.local.json";
3
+ /** The schema's path inside this package. */
4
+ export declare const SCHEMA_FILE = "config/agentic.config.schema.json";
5
+ export declare const SCHEMA_PATH: string;
6
+ export declare const COMMIT_CONVENTIONS: readonly ["ticket-key", "conventional"];
7
+ export type CommitConvention = (typeof COMMIT_CONVENTIONS)[number];
8
+ export type NodePackageManager = "pnpm" | "npm" | "yarn" | "bun";
9
+ export type ForcePush = "deny" | "ask" | "lease";
10
+ export type CommitOnProtected = "deny" | "allow";
11
+ export type EnvPolicy = "deny" | "git";
12
+ /** Which env files may be read: `deny` list, then `allow` list, then `policy` for the rest. */
13
+ export type EnvFiles = {
14
+ policy: EnvPolicy;
15
+ allow: string[];
16
+ deny: string[];
17
+ };
18
+ export type Guardrails = {
19
+ forcePush: ForcePush;
20
+ commitOnProtected: CommitOnProtected;
21
+ envFiles: EnvFiles;
22
+ protectedBranches: string[];
23
+ };
24
+ /** `guardrails` as one layer writes it: every key optional, `envFiles` key by key. */
25
+ export type GuardrailsLayer = Partial<Omit<Guardrails, "envFiles">> & {
26
+ envFiles?: Partial<EnvFiles>;
27
+ };
28
+ /** A repository's own script under `gate.checks.run`, reported as a Check under `name`. */
29
+ export type RunCheck = {
30
+ name: string;
31
+ command: string;
32
+ };
33
+ /**
34
+ * `run`, and under each other key a Check the package ships, as the schema takes it: `true`, `false` or
35
+ * its options. `src/checks/` reads the setting of each.
36
+ */
37
+ export type GateChecks = {
38
+ run: RunCheck[];
39
+ [check: string]: unknown;
40
+ };
41
+ /** The Gate: its command, the steps run in order, the Checks run after them. */
42
+ export type Gate = {
43
+ command?: string;
44
+ steps: string[];
45
+ checks: GateChecks;
46
+ };
47
+ /** `gate` as one layer writes it: every key optional, `checks` Check by Check. */
48
+ export type GateLayer = {
49
+ command?: string;
50
+ steps?: string[];
51
+ checks?: Partial<GateChecks>;
52
+ };
53
+ /** One layer as written in a file: every key optional, unknown top-level keys tolerated. */
54
+ export type AgenticConfig = {
55
+ $schema?: string;
56
+ version?: 1;
57
+ baseBranch?: string;
58
+ packageManagers?: {
59
+ node?: NodePackageManager;
60
+ ruby?: "bundler";
61
+ };
62
+ guardrails?: GuardrailsLayer;
63
+ commit?: {
64
+ convention?: CommitConvention;
65
+ pattern?: string;
66
+ trailer?: string;
67
+ };
68
+ gate?: GateLayer;
69
+ worktree?: {
70
+ setup?: string[];
71
+ };
72
+ };
73
+ /** What the guards see: every layer applied, every key present. */
74
+ export type ResolvedConfig = {
75
+ version: 1;
76
+ baseBranch: string;
77
+ packageManagers: {
78
+ node?: NodePackageManager;
79
+ ruby?: "bundler";
80
+ };
81
+ guardrails: Guardrails;
82
+ commit: {
83
+ convention: CommitConvention;
84
+ pattern: string;
85
+ trailer?: string;
86
+ };
87
+ gate: Gate;
88
+ worktree: {
89
+ setup: string[];
90
+ };
91
+ };
92
+ /** The layers merged, before `commit.pattern` falls back to the preset its convention names. */
93
+ type Merged = Omit<ResolvedConfig, "commit"> & {
94
+ commit: {
95
+ convention: CommitConvention;
96
+ pattern?: string;
97
+ trailer?: string;
98
+ };
99
+ };
100
+ export type Problem = {
101
+ file: string;
102
+ path: string;
103
+ message: string;
104
+ };
105
+ export type LayerState = "absent" | "valid" | "invalid";
106
+ export type Resolution = {
107
+ config: ResolvedConfig;
108
+ problems: Problem[];
109
+ layers: {
110
+ committed: LayerState;
111
+ local: LayerState;
112
+ };
113
+ /** The checkout each layer was read from: the root, or the main checkout for a worktree lacking it. */
114
+ sources: {
115
+ committed: string | undefined;
116
+ local: string | undefined;
117
+ };
118
+ };
119
+ /** Strictest value first; the local override may only move towards the front. */
120
+ export declare const STRICTNESS: {
121
+ readonly forcePush: readonly ["deny", "ask", "lease"];
122
+ readonly commitOnProtected: readonly ["deny", "allow"];
123
+ readonly envFiles: readonly ["deny", "git"];
124
+ };
125
+ /** What the guards run on while any layer is invalid. */
126
+ export declare const STRICTEST_GUARDRAILS: Guardrails;
127
+ /** What a repository gets with nothing declared. */
128
+ export declare const DEFAULT_GUARDRAILS: Guardrails;
129
+ /**
130
+ * Whether an `envFiles` glob selects the file at `relativePath` (to the repository root, or the
131
+ * basename outside one). `*` and `?` as in a shell pattern, `*` crossing `/`; a leading `**` and a
132
+ * slash mean at any depth. The same rule `hooks/block-env-access.sh` applies with `[[ == ]]`.
133
+ */
134
+ export declare function matchesEnvGlob(glob: string, relativePath: string): boolean;
135
+ export declare const COMMIT_PATTERNS: Record<CommitConvention, string>;
136
+ /**
137
+ * The schema's public `$id`: what a written config points its `$schema` at. Stable across plugin
138
+ * installs, where `SCHEMA_PATH` moves with every version.
139
+ */
140
+ export declare function schemaId(): string;
141
+ /** Parses and validates one document; `file` names it in every problem. */
142
+ export declare function validateDocument(file: string, text: string): {
143
+ value: AgenticConfig;
144
+ } | {
145
+ problems: Problem[];
146
+ };
147
+ /** The top level of the repository holding `dir`, or undefined outside one. */
148
+ export declare function findRepository(dir: string): string | undefined;
149
+ /** The top level of the repository holding `dir`, or `dir` itself outside one. */
150
+ export declare function repositoryRoot(dir: string): string;
151
+ /**
152
+ * The main checkout of the linked worktree at `root`, or undefined for a main checkout or outside a
153
+ * repository: a linked worktree's git dir differs from the common one, which sits in the main checkout.
154
+ */
155
+ export declare function mainCheckout(root: string): string | undefined;
156
+ /**
157
+ * The bottom layer: the package manager the lockfile implies, the Gate command the manifest's `check`
158
+ * script implies, plus the defaults every key falls back to. `commit.pattern` is left to
159
+ * `resolveConfig`, which fills it from the convention when no layer sets one.
160
+ */
161
+ export declare function inferConfig(root: string): Merged;
162
+ /**
163
+ * Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
164
+ * lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
165
+ */
166
+ export declare function resolveConfig(root: string): Resolution;
167
+ export {};
@@ -0,0 +1,305 @@
1
+ // Reads what a repository declares for an agent: the committed config, the machine's local layer over it,
2
+ // and what the repository implies; an invalid layer is dropped whole. The guardrails plugin's
3
+ // `hooks/config.jq` is the same merge for the guards; this file is the reference, and the tests run both.
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { validate } from "./json-schema.js";
8
+ export const COMMITTED_FILE = ".ai/agentic.config.json";
9
+ export const LOCAL_FILE = ".ai/agentic.config.local.json";
10
+ /** The schema's path inside this package. */
11
+ export const SCHEMA_FILE = "config/agentic.config.schema.json";
12
+ export const SCHEMA_PATH = join(import.meta.dirname, "..", SCHEMA_FILE);
13
+ export const COMMIT_CONVENTIONS = ["ticket-key", "conventional"];
14
+ /** Strictest value first; the local override may only move towards the front. */
15
+ export const STRICTNESS = {
16
+ forcePush: ["deny", "ask", "lease"],
17
+ commitOnProtected: ["deny", "allow"],
18
+ envFiles: ["deny", "git"]
19
+ };
20
+ /** What the guards run on while any layer is invalid. */
21
+ export const STRICTEST_GUARDRAILS = {
22
+ forcePush: "deny",
23
+ commitOnProtected: "deny",
24
+ envFiles: { policy: "deny", allow: [], deny: [] },
25
+ protectedBranches: ["master", "main", "production"]
26
+ };
27
+ /** What a repository gets with nothing declared. */
28
+ export const DEFAULT_GUARDRAILS = {
29
+ forcePush: "lease",
30
+ commitOnProtected: "allow",
31
+ envFiles: { policy: "git", allow: ["**/.env.example", "**/.env.sample", "**/.env.template"], deny: [] },
32
+ protectedBranches: ["master", "main", "production"]
33
+ };
34
+ function cloneGuardrails(source) {
35
+ return {
36
+ forcePush: source.forcePush,
37
+ commitOnProtected: source.commitOnProtected,
38
+ envFiles: { policy: source.envFiles.policy, allow: [...source.envFiles.allow], deny: [...source.envFiles.deny] },
39
+ protectedBranches: [...source.protectedBranches]
40
+ };
41
+ }
42
+ /**
43
+ * Whether an `envFiles` glob selects the file at `relativePath` (to the repository root, or the
44
+ * basename outside one). `*` and `?` as in a shell pattern, `*` crossing `/`; a leading `**` and a
45
+ * slash mean at any depth. The same rule `hooks/block-env-access.sh` applies with `[[ == ]]`.
46
+ */
47
+ export function matchesEnvGlob(glob, relativePath) {
48
+ const anyDepth = glob.startsWith("**/");
49
+ const pattern = anyDepth ? glob.slice(3) : glob;
50
+ const source = pattern
51
+ .split("")
52
+ .map((char) => {
53
+ if (char === "*") {
54
+ return ".*";
55
+ }
56
+ if (char === "?") {
57
+ return ".";
58
+ }
59
+ return char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
60
+ })
61
+ .join("");
62
+ return new RegExp(`^${anyDepth ? "(.*/)?" : ""}${source}$`).test(relativePath);
63
+ }
64
+ // The preset each convention names. `ticket-key` is a ticket key ahead of a short type list;
65
+ // `conventional` is Conventional Commits. Both are POSIX ERE, because `grep -E` runs them.
66
+ export const COMMIT_PATTERNS = {
67
+ "ticket-key": "^\\[[A-Z]+-[0-9]+\\] (feat|fix|refactor|docs|chore|test|perf|ci|revert): .+",
68
+ conventional: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([^)]+\\))?!?: .+"
69
+ };
70
+ const NODE_LOCKFILES = [
71
+ ["pnpm-lock.yaml", "pnpm"],
72
+ ["package-lock.json", "npm"],
73
+ ["yarn.lock", "yarn"],
74
+ ["bun.lock", "bun"],
75
+ ["bun.lockb", "bun"]
76
+ ];
77
+ function messageOf(error) {
78
+ return error instanceof Error ? error.message : String(error);
79
+ }
80
+ function show(value) {
81
+ return JSON.stringify(value);
82
+ }
83
+ let schema;
84
+ function loadSchema() {
85
+ schema ??= JSON.parse(readFileSync(SCHEMA_PATH, "utf8"));
86
+ return schema;
87
+ }
88
+ /**
89
+ * The schema's public `$id`: what a written config points its `$schema` at. Stable across plugin
90
+ * installs, where `SCHEMA_PATH` moves with every version.
91
+ */
92
+ export function schemaId() {
93
+ const loaded = loadSchema();
94
+ if (typeof loaded === "object" && loaded !== null && "$id" in loaded && typeof loaded.$id === "string") {
95
+ return loaded.$id;
96
+ }
97
+ throw new Error(`${SCHEMA_PATH} declares no string $id`);
98
+ }
99
+ /** Parses and validates one document; `file` names it in every problem. */
100
+ export function validateDocument(file, text) {
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(text);
104
+ }
105
+ catch (error) {
106
+ return { problems: [{ file, path: "", message: `not JSON: ${messageOf(error)}` }] };
107
+ }
108
+ const problems = validate(loadSchema(), parsed).map(({ path, message }) => ({ file, path, message }));
109
+ if (problems.length > 0) {
110
+ return { problems };
111
+ }
112
+ // The schema is the runtime twin of `AgenticConfig`: a document it accepts has this shape.
113
+ return { value: parsed };
114
+ }
115
+ /** `file` from the first of `checkouts` that has it. */
116
+ function readLayer(checkouts, file) {
117
+ const source = checkouts.find((checkout) => existsSync(join(checkout, file)));
118
+ if (source === undefined) {
119
+ return { state: "absent", problems: [] };
120
+ }
121
+ const result = validateDocument(file, readFileSync(join(source, file), "utf8"));
122
+ if ("problems" in result) {
123
+ return { state: "invalid", problems: result.problems, source };
124
+ }
125
+ return { state: "valid", value: result.value, problems: [], source };
126
+ }
127
+ /** The top level of the repository holding `dir`, or undefined outside one. */
128
+ export function findRepository(dir) {
129
+ const toplevel = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
130
+ return toplevel.status === 0 ? toplevel.stdout.trim() : undefined;
131
+ }
132
+ /** The top level of the repository holding `dir`, or `dir` itself outside one. */
133
+ export function repositoryRoot(dir) {
134
+ return findRepository(dir) ?? dir;
135
+ }
136
+ /**
137
+ * The main checkout of the linked worktree at `root`, or undefined for a main checkout or outside a
138
+ * repository: a linked worktree's git dir differs from the common one, which sits in the main checkout.
139
+ */
140
+ export function mainCheckout(root) {
141
+ const args = ["-C", root, "rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"];
142
+ const result = spawnSync("git", args, { encoding: "utf8" });
143
+ if (result.status !== 0) {
144
+ return undefined;
145
+ }
146
+ const [gitDir, commonDir] = result.stdout.trim().split("\n");
147
+ return gitDir === undefined || commonDir === undefined || gitDir === commonDir ? undefined : dirname(commonDir);
148
+ }
149
+ function inferPackageManagers(root) {
150
+ const managers = {};
151
+ const lockfile = NODE_LOCKFILES.find(([file]) => existsSync(join(root, file)));
152
+ if (lockfile !== undefined) {
153
+ managers.node = lockfile[1];
154
+ }
155
+ return managers;
156
+ }
157
+ /** Whether `package.json` at `root` declares a `check` script; a missing or unreadable manifest has none. */
158
+ function hasCheckScript(root) {
159
+ let manifest;
160
+ try {
161
+ manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ if (typeof manifest !== "object" || manifest === null || !("scripts" in manifest)) {
167
+ return false;
168
+ }
169
+ const { scripts } = manifest;
170
+ return typeof scripts === "object" && scripts !== null && "check" in scripts && typeof scripts.check === "string";
171
+ }
172
+ /** The Gate with nothing declared: `pnpm check` where the manifest has a `check` script, else no command. */
173
+ function inferGate(root) {
174
+ const gate = { steps: [], checks: { run: [] } };
175
+ if (hasCheckScript(root)) {
176
+ gate.command = "pnpm check";
177
+ }
178
+ return gate;
179
+ }
180
+ /**
181
+ * The bottom layer: the package manager the lockfile implies, the Gate command the manifest's `check`
182
+ * script implies, plus the defaults every key falls back to. `commit.pattern` is left to
183
+ * `resolveConfig`, which fills it from the convention when no layer sets one.
184
+ */
185
+ export function inferConfig(root) {
186
+ return {
187
+ version: 1,
188
+ baseBranch: "auto",
189
+ packageManagers: inferPackageManagers(root),
190
+ guardrails: cloneGuardrails(DEFAULT_GUARDRAILS),
191
+ commit: { convention: "conventional" },
192
+ gate: inferGate(root),
193
+ worktree: { setup: [] }
194
+ };
195
+ }
196
+ function applyLayer(base, layer) {
197
+ return {
198
+ version: 1,
199
+ baseBranch: layer.baseBranch ?? base.baseBranch,
200
+ packageManagers: { ...base.packageManagers, ...layer.packageManagers },
201
+ guardrails: {
202
+ ...base.guardrails,
203
+ ...layer.guardrails,
204
+ envFiles: { ...base.guardrails.envFiles, ...layer.guardrails?.envFiles }
205
+ },
206
+ commit: { ...base.commit, ...layer.commit },
207
+ gate: { ...base.gate, ...layer.gate, checks: { ...base.gate.checks, ...layer.gate?.checks } },
208
+ worktree: { setup: layer.worktree?.setup ?? base.worktree.setup }
209
+ };
210
+ }
211
+ function rank(key, value) {
212
+ const order = STRICTNESS[key];
213
+ return order.indexOf(value);
214
+ }
215
+ const LOOSENS = "the local file may tighten guardrails, never loosen them";
216
+ /**
217
+ * Every guardrail the local layer sets less strictly than what it would override: a lower-ranked
218
+ * `forcePush`, `commitOnProtected` or `envFiles.policy`, an added `envFiles.allow` entry, a dropped
219
+ * `envFiles.deny` entry, a dropped protected branch.
220
+ */
221
+ function loosenings(base, local) {
222
+ if (local === undefined) {
223
+ return [];
224
+ }
225
+ const problems = [];
226
+ if (local.forcePush !== undefined && rank("forcePush", local.forcePush) > rank("forcePush", base.forcePush)) {
227
+ const message = `${show(local.forcePush)} loosens ${show(base.forcePush)}; ${LOOSENS}`;
228
+ problems.push({ file: LOCAL_FILE, path: "guardrails.forcePush", message });
229
+ }
230
+ if (local.commitOnProtected !== undefined &&
231
+ rank("commitOnProtected", local.commitOnProtected) > rank("commitOnProtected", base.commitOnProtected)) {
232
+ const message = `${show(local.commitOnProtected)} loosens ${show(base.commitOnProtected)}; ${LOOSENS}`;
233
+ problems.push({ file: LOCAL_FILE, path: "guardrails.commitOnProtected", message });
234
+ }
235
+ const policy = local.envFiles?.policy;
236
+ if (policy !== undefined && rank("envFiles", policy) > rank("envFiles", base.envFiles.policy)) {
237
+ const message = `${show(policy)} loosens ${show(base.envFiles.policy)}; ${LOOSENS}`;
238
+ problems.push({ file: LOCAL_FILE, path: "guardrails.envFiles.policy", message });
239
+ }
240
+ if (local.envFiles?.allow !== undefined) {
241
+ const known = new Set(base.envFiles.allow);
242
+ const added = local.envFiles.allow.filter((glob) => !known.has(glob));
243
+ if (added.length > 0) {
244
+ const message = `adds ${added.map(show).join(", ")}; the local file may drop allowed env files, never add them`;
245
+ problems.push({ file: LOCAL_FILE, path: "guardrails.envFiles.allow", message });
246
+ }
247
+ }
248
+ if (local.envFiles?.deny !== undefined) {
249
+ const kept = new Set(local.envFiles.deny);
250
+ const dropped = base.envFiles.deny.filter((glob) => !kept.has(glob));
251
+ if (dropped.length > 0) {
252
+ const message = `drops ${dropped.map(show).join(", ")}; the local file may add denied env files, never drop them`;
253
+ problems.push({ file: LOCAL_FILE, path: "guardrails.envFiles.deny", message });
254
+ }
255
+ }
256
+ if (local.protectedBranches !== undefined) {
257
+ const kept = new Set(local.protectedBranches);
258
+ const dropped = base.protectedBranches.filter((branch) => !kept.has(branch));
259
+ if (dropped.length > 0) {
260
+ const message = `drops ${dropped.map(show).join(", ")}; the local file may add protected branches, never remove them`;
261
+ problems.push({ file: LOCAL_FILE, path: "guardrails.protectedBranches", message });
262
+ }
263
+ }
264
+ return problems;
265
+ }
266
+ /** `commit.pattern` from the convention's preset when no layer set one; a set pattern wins. */
267
+ function finalize(merged) {
268
+ const pattern = merged.commit.pattern ?? COMMIT_PATTERNS[merged.commit.convention];
269
+ return { ...merged, commit: { ...merged.commit, pattern } };
270
+ }
271
+ /**
272
+ * Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
273
+ * lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
274
+ */
275
+ export function resolveConfig(root) {
276
+ const shared = mainCheckout(root);
277
+ const checkouts = shared === undefined ? [root] : [root, shared];
278
+ const committed = readLayer(checkouts, COMMITTED_FILE);
279
+ const local = readLayer(checkouts, LOCAL_FILE);
280
+ const problems = [...committed.problems, ...local.problems];
281
+ let localState = local.state;
282
+ let config = inferConfig(root);
283
+ if (committed.value !== undefined) {
284
+ config = applyLayer(config, committed.value);
285
+ }
286
+ if (local.value !== undefined) {
287
+ const loosened = loosenings(config.guardrails, local.value.guardrails);
288
+ if (loosened.length > 0) {
289
+ problems.push(...loosened);
290
+ localState = "invalid";
291
+ }
292
+ else {
293
+ config = applyLayer(config, local.value);
294
+ }
295
+ }
296
+ if (problems.length > 0) {
297
+ config = { ...config, guardrails: cloneGuardrails(STRICTEST_GUARDRAILS) };
298
+ }
299
+ return {
300
+ config: finalize(config),
301
+ problems,
302
+ layers: { committed: committed.state, local: localState },
303
+ sources: { committed: committed.source, local: local.source }
304
+ };
305
+ }
@@ -0,0 +1,36 @@
1
+ export type Group = {
2
+ id: string;
3
+ name: string;
4
+ };
5
+ export type Primitive = {
6
+ id: string;
7
+ name: string;
8
+ group: string;
9
+ summary: string;
10
+ code: string[];
11
+ docs: string[];
12
+ };
13
+ export type Catalogue = {
14
+ version: 1;
15
+ groups: Group[];
16
+ primitives: Primitive[];
17
+ };
18
+ /** Where a repository keeps its catalogue unless its config says otherwise. */
19
+ export declare const CATALOGUE_FILE = ".ai/primitives.json";
20
+ export type CatalogueRead = {
21
+ state: "missing";
22
+ } | {
23
+ state: "invalid";
24
+ problems: string[];
25
+ } | {
26
+ state: "valid";
27
+ catalogue: Catalogue;
28
+ text: string;
29
+ };
30
+ /** The catalogue at `file` under `root`, or why there is none to read. */
31
+ export declare function readCatalogue(root: string, file: string): CatalogueRead;
32
+ /**
33
+ * The Primitives whose `code` names `path`, relative to the repository root: of those, the ones whose
34
+ * entry is nearest the path, so a file wins over the directory holding it. Empty when none names it.
35
+ */
36
+ export declare function primitivesNaming(catalogue: Catalogue, path: string): Primitive[];
@@ -0,0 +1,74 @@
1
+ // The Primitives catalogue: what a repository exposes to an agent, where the code of each lives and
2
+ // which documents explain it. Read and held to its shape here; the `primitives` Check judges what it
3
+ // says and `dw-sdlc primitives --paths` answers from it.
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { validate } from "./json-schema.js";
7
+ /** Where a repository keeps its catalogue unless its config says otherwise. */
8
+ export const CATALOGUE_FILE = ".ai/primitives.json";
9
+ const STRINGS = { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 };
10
+ const NAMED = { type: "string", minLength: 1 };
11
+ const SCHEMA = {
12
+ type: "object",
13
+ required: ["version", "groups", "primitives"],
14
+ properties: {
15
+ version: { const: 1 },
16
+ groups: {
17
+ type: "array",
18
+ items: {
19
+ type: "object",
20
+ required: ["id", "name"],
21
+ properties: { id: NAMED, name: NAMED },
22
+ additionalProperties: false
23
+ }
24
+ },
25
+ primitives: {
26
+ type: "array",
27
+ items: {
28
+ type: "object",
29
+ required: ["id", "name", "group", "summary", "code", "docs"],
30
+ properties: { id: NAMED, name: NAMED, group: NAMED, summary: NAMED, code: STRINGS, docs: STRINGS },
31
+ additionalProperties: false
32
+ }
33
+ }
34
+ },
35
+ additionalProperties: false
36
+ };
37
+ /** The catalogue at `file` under `root`, or why there is none to read. */
38
+ export function readCatalogue(root, file) {
39
+ const path = join(root, file);
40
+ if (!existsSync(path)) {
41
+ return { state: "missing" };
42
+ }
43
+ const text = readFileSync(path, "utf8");
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(text);
47
+ }
48
+ catch (error) {
49
+ return { state: "invalid", problems: [`not JSON: ${error instanceof Error ? error.message : String(error)}`] };
50
+ }
51
+ const problems = validate(SCHEMA, parsed).map(({ path: at, message }) => (at === "" ? message : `${at}: ${message}`));
52
+ if (problems.length > 0) {
53
+ return { state: "invalid", problems };
54
+ }
55
+ // The schema above is the runtime twin of `Catalogue`: a document it accepts has this shape.
56
+ return { state: "valid", catalogue: parsed, text };
57
+ }
58
+ /**
59
+ * How near the `code` entry `entry`, a file or a directory, names `path`: the length of the entry when
60
+ * it is the path or a directory above it, -1 when it is neither.
61
+ */
62
+ function covers(entry, path) {
63
+ const prefix = entry.replace(/\/+$/, "");
64
+ return path === prefix || path.startsWith(`${prefix}/`) ? prefix.length : -1;
65
+ }
66
+ /**
67
+ * The Primitives whose `code` names `path`, relative to the repository root: of those, the ones whose
68
+ * entry is nearest the path, so a file wins over the directory holding it. Empty when none names it.
69
+ */
70
+ export function primitivesNaming(catalogue, path) {
71
+ const nearest = catalogue.primitives.map((primitive) => Math.max(-1, ...primitive.code.map((entry) => covers(entry, path))));
72
+ const best = Math.max(-1, ...nearest);
73
+ return best === -1 ? [] : catalogue.primitives.filter((_, index) => nearest[index] === best);
74
+ }
@@ -0,0 +1,7 @@
1
+ import { type Check } from "./check.ts";
2
+ export type AgentsMapOptions = {
3
+ file: string;
4
+ maxLines: number;
5
+ maxBytes: number;
6
+ };
7
+ export declare const agentsMap: Check<AgentsMapOptions>;