@flancer32/skill-adsm-ctx 0.2.1

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 (132) hide show
  1. package/.github/workflows/flows-app.yaml +95 -0
  2. package/AGENTS.md +112 -0
  3. package/LICENSE +201 -0
  4. package/README.md +246 -0
  5. package/bin/cli.mjs +55 -0
  6. package/ctx/AGENTS.md +44 -0
  7. package/ctx/README.md +24 -0
  8. package/ctx/adsm.json +5 -0
  9. package/ctx/agent/AGENTS.md +41 -0
  10. package/ctx/agent/flows/.img/flancer32-skill-adsm-ctx-github-flows.png +0 -0
  11. package/ctx/agent/flows/AGENTS.md +37 -0
  12. package/ctx/agent/flows/cfg/issues/labeled/accepted/profile.json +25 -0
  13. package/ctx/agent/flows/cfg/issues/labeled/accepted/prompt.md +104 -0
  14. package/ctx/agent/flows/cfg/issues/labeled/clarification-provided/profile.json +15 -0
  15. package/ctx/agent/flows/cfg/issues/labeled/clarification-provided/prompt.md +63 -0
  16. package/ctx/agent/flows/cfg/issues/opened/admission/profile.json +14 -0
  17. package/ctx/agent/flows/cfg/issues/opened/admission/prompt.md +110 -0
  18. package/ctx/agent/flows/cfg/profile.json +40 -0
  19. package/ctx/agent/flows/cfg/pull_request/closed/merged/profile.json +15 -0
  20. package/ctx/agent/flows/cfg/pull_request/closed/merged/prompt.md +41 -0
  21. package/ctx/agent/flows/cfg/pull_request/labeled/needs-fix/profile.json +25 -0
  22. package/ctx/agent/flows/cfg/pull_request/labeled/needs-fix/prompt.md +79 -0
  23. package/ctx/agent/flows/cfg/pull_request/labeled/ready-for-validation/profile.json +15 -0
  24. package/ctx/agent/flows/cfg/pull_request/labeled/ready-for-validation/prompt.md +72 -0
  25. package/ctx/agent/flows/cfg/pull_request/labeled/verified/profile.json +15 -0
  26. package/ctx/agent/flows/cfg/pull_request/labeled/verified/prompt.md +48 -0
  27. package/ctx/agent/flows/deployment.md +116 -0
  28. package/ctx/agent/flows/labels.md +108 -0
  29. package/ctx/agent/flows/overview.md +82 -0
  30. package/ctx/agent/prompt/cli/ai-intro.md +141 -0
  31. package/ctx/assets/.gitkeep +1 -0
  32. package/ctx/docs/AGENTS.md +121 -0
  33. package/ctx/docs/ai-intro.md +37 -0
  34. package/ctx/docs/architecture/AGENTS.md +98 -0
  35. package/ctx/docs/architecture/behavior.md +68 -0
  36. package/ctx/docs/architecture/checklists.md +48 -0
  37. package/ctx/docs/architecture/constraints.md +53 -0
  38. package/ctx/docs/architecture/constraints.skin.en.md +38 -0
  39. package/ctx/docs/architecture/decisions.md +118 -0
  40. package/ctx/docs/architecture/integration.md +31 -0
  41. package/ctx/docs/architecture/overview.md +76 -0
  42. package/ctx/docs/architecture/state.md +50 -0
  43. package/ctx/docs/architecture/structure.md +38 -0
  44. package/ctx/docs/architecture/supervision.md +71 -0
  45. package/ctx/docs/code/AGENTS.md +51 -0
  46. package/ctx/docs/code/overview.md +53 -0
  47. package/ctx/docs/code/testing.md +26 -0
  48. package/ctx/docs/environment/AGENTS.md +50 -0
  49. package/ctx/docs/environment/overview.md +33 -0
  50. package/ctx/docs/filesystem.md +44 -0
  51. package/ctx/docs/product/AGENTS.md +51 -0
  52. package/ctx/docs/product/domain.md +63 -0
  53. package/ctx/docs/product/domain.skin.en.md +40 -0
  54. package/ctx/docs/product/glossary.md +31 -0
  55. package/ctx/docs/product/overview.md +112 -0
  56. package/ctx/docs/product/overview.skin.en.md +38 -0
  57. package/ctx/docs/product/roles.md +17 -0
  58. package/ctx/docs/product/use-cases.md +15 -0
  59. package/jsconfig.json +12 -0
  60. package/migrations/README.md +9 -0
  61. package/package.json +33 -0
  62. package/skill/SKILL.md +47 -0
  63. package/skill/references/agents-hierarchy.md +45 -0
  64. package/skill/references/context-lifecycle.md +53 -0
  65. package/skill/references/design-questionnaire.md +44 -0
  66. package/skill/references/document-projections.md +290 -0
  67. package/skill/references/documentation-compression.md +117 -0
  68. package/skill/references/documentation-quality-audit.md +199 -0
  69. package/skill/references/orchestration-model.md +41 -0
  70. package/skill/references/specialized-skills.md +31 -0
  71. package/skill/references/upgrade-policy.md +19 -0
  72. package/skill/templates/AGENTS.md +120 -0
  73. package/skill/templates/ai/AGENTS.md +59 -0
  74. package/skill/templates/ctx/AGENTS.md +45 -0
  75. package/skill/templates/ctx/README.md +27 -0
  76. package/skill/templates/ctx/adsm.json +7 -0
  77. package/skill/templates/ctx/agent/AGENTS.md +36 -0
  78. package/skill/templates/ctx/assets/.gitkeep +1 -0
  79. package/skill/templates/ctx/docs/AGENTS.md +55 -0
  80. package/skill/templates/ctx/docs/ai-intro.md +53 -0
  81. package/skill/templates/ctx/docs/architecture/AGENTS.md +42 -0
  82. package/skill/templates/ctx/docs/architecture/behavior.md +38 -0
  83. package/skill/templates/ctx/docs/architecture/checklists.md +25 -0
  84. package/skill/templates/ctx/docs/architecture/constraints.md +36 -0
  85. package/skill/templates/ctx/docs/architecture/decisions.md +33 -0
  86. package/skill/templates/ctx/docs/architecture/integration.md +27 -0
  87. package/skill/templates/ctx/docs/architecture/overview.md +73 -0
  88. package/skill/templates/ctx/docs/architecture/state.md +52 -0
  89. package/skill/templates/ctx/docs/architecture/structure.md +32 -0
  90. package/skill/templates/ctx/docs/architecture/supervision.md +61 -0
  91. package/skill/templates/ctx/docs/code/AGENTS.md +35 -0
  92. package/skill/templates/ctx/docs/code/overview.md +21 -0
  93. package/skill/templates/ctx/docs/code/testing.md +13 -0
  94. package/skill/templates/ctx/docs/environment/AGENTS.md +34 -0
  95. package/skill/templates/ctx/docs/environment/overview.md +21 -0
  96. package/skill/templates/ctx/docs/filesystem.md +39 -0
  97. package/skill/templates/ctx/docs/product/AGENTS.md +38 -0
  98. package/skill/templates/ctx/docs/product/domain.md +77 -0
  99. package/skill/templates/ctx/docs/product/glossary.md +110 -0
  100. package/skill/templates/ctx/docs/product/overview.md +74 -0
  101. package/skill/templates/ctx/docs/product/roles.md +100 -0
  102. package/skill/templates/ctx/docs/product/use-cases.md +109 -0
  103. package/skill/templates/work/context/semantic-skin.md +27 -0
  104. package/skill/templates/work/report/documentation-compression.md +34 -0
  105. package/skill/templates/work/report/documentation-quality-audit.md +90 -0
  106. package/src/Bootstrap/Cli.mjs +62 -0
  107. package/src/Bootstrap.mjs +108 -0
  108. package/src/Design/Service.mjs +36 -0
  109. package/src/Init/Service.mjs +136 -0
  110. package/src/Shared/Constants.mjs +36 -0
  111. package/src/Shared/CopyDirectory.mjs +78 -0
  112. package/src/Shared/Metadata.mjs +30 -0
  113. package/src/Shared/Path.mjs +38 -0
  114. package/src/Skill/Installer.mjs +47 -0
  115. package/src/Upgrade/Service.mjs +44 -0
  116. package/src/Validate/Service.mjs +879 -0
  117. package/test/integration/.gitkeep +1 -0
  118. package/test/integration/cli-help.test.mjs +15 -0
  119. package/test/integration/cli-init-validate.test.mjs +223 -0
  120. package/test/integration/cli-install-skill.test.mjs +41 -0
  121. package/test/unit/Bootstrap/Cli.test.mjs +23 -0
  122. package/test/unit/Bootstrap.test.mjs +53 -0
  123. package/test/unit/Design/Service.test.mjs +11 -0
  124. package/test/unit/Init/Service.test.mjs +51 -0
  125. package/test/unit/Shared/Constants.test.mjs +21 -0
  126. package/test/unit/Shared/CopyDirectory.test.mjs +31 -0
  127. package/test/unit/Shared/Metadata.test.mjs +9 -0
  128. package/test/unit/Shared/Path.test.mjs +11 -0
  129. package/test/unit/Skill/Installer.test.mjs +20 -0
  130. package/test/unit/Upgrade/Service.test.mjs +13 -0
  131. package/test/unit/Validate/Service.test.mjs +760 -0
  132. package/types.d.ts +15 -0
@@ -0,0 +1,879 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @namespace Fl32_Skill_Adsm_Ctx_Validate_Service
5
+ * @description Validates baseline ADSM context structure, metadata, and documentation projection shape.
6
+ */
7
+
8
+ const REQUIRED_SKIN_SECTIONS = Object.freeze([
9
+ "Purpose",
10
+ "Mental Model",
11
+ "Scope",
12
+ "Invariants",
13
+ "Agent Document",
14
+ ]);
15
+
16
+ const LINK_PATTERN = /\[[^\]]+\]\(([^)]+)\)/g;
17
+ const EXPLICIT_SKIN_FILENAME = /^(.*)\.skin\.([a-z]{2}(?:-[A-Za-z0-9]+)?)\.md$/;
18
+ const LEGACY_SKIN_FILENAME = /^(.*)\.skin\.md$/;
19
+ const MALFORMED_SKIN_FILENAME = /^(.*)\.skin\.([^.]+)\.md$/;
20
+
21
+ export default class Validate {
22
+ /**
23
+ * @param {object} deps
24
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Constants} deps.constants
25
+ * @param {typeof import("node:fs")} deps.fs
26
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Path} deps.pathService
27
+ */
28
+ constructor({constants, fs, pathService}) {
29
+ /**
30
+ * Validates context structure.
31
+ *
32
+ * @param {object} params
33
+ * @param {string} params.targetPath
34
+ * @returns {Promise<{
35
+ * errorCount: number,
36
+ * findings: {
37
+ * message: string,
38
+ * path: string,
39
+ * ruleId: string,
40
+ * severity: "error" | "warning"
41
+ * }[],
42
+ * metadata: {schema: string, version: string, initializedAt?: string} | null,
43
+ * missingPaths: string[],
44
+ * ok: boolean,
45
+ * projectRoot: string,
46
+ * projection: {
47
+ * assets: string[],
48
+ * bodies: string[],
49
+ * orphanSkins: string[],
50
+ * pairs: {body: string, skin: string}[],
51
+ * skins: string[]
52
+ * },
53
+ * warningCount: number
54
+ * }>}
55
+ */
56
+ this.execute = async function ({targetPath}) {
57
+ const projectRoot = pathService.resolve(targetPath);
58
+ const missingPaths = constants.REQUIRED_PATHS.filter((entry) => !fs.existsSync(pathService.join(projectRoot, entry)));
59
+ const findings = missingPaths.map((entry) => createFinding({
60
+ message: `Missing: ${entry}`,
61
+ path: entry,
62
+ ruleId: "required-path",
63
+ severity: "error",
64
+ }));
65
+ const metadata = readMetadata({constants, fs, pathService, projectRoot});
66
+ const projection = inspectProjection({constants, fs, pathService, projectRoot});
67
+
68
+ if (!metadata.ok) findings.push(metadata.finding);
69
+ findings.push(...projection.findings);
70
+
71
+ const errorCount = findings.filter((entry) => entry.severity === "error").length;
72
+ const warningCount = findings.filter((entry) => entry.severity === "warning").length;
73
+
74
+ return {
75
+ errorCount,
76
+ findings,
77
+ metadata: metadata.ok ? metadata.metadata : null,
78
+ missingPaths,
79
+ ok: errorCount === 0,
80
+ projectRoot,
81
+ projection: projection.summary,
82
+ warningCount,
83
+ };
84
+ };
85
+
86
+ /**
87
+ * Formats validation result for CLI output.
88
+ *
89
+ * @param {object} params
90
+ * @param {{
91
+ * errorCount: number,
92
+ * findings: {
93
+ * message: string,
94
+ * path: string,
95
+ * ruleId: string,
96
+ * severity: "error" | "warning"
97
+ * }[],
98
+ * metadata: {version: string} | null,
99
+ * ok: boolean,
100
+ * projectRoot: string,
101
+ * warningCount: number
102
+ * }} params.result
103
+ * @returns {string}
104
+ */
105
+ this.format = function ({result}) {
106
+ const status = result.errorCount > 0
107
+ ? "ADSM context validation failed."
108
+ : result.warningCount > 0
109
+ ? "ADSM context validation passed with warnings."
110
+ : "ADSM context validation passed.";
111
+
112
+ const lines = [
113
+ status,
114
+ `Project root: ${result.projectRoot}`,
115
+ `Context version: ${result.metadata?.version ?? "unknown"}`,
116
+ `Errors: ${result.errorCount}`,
117
+ `Warnings: ${result.warningCount}`,
118
+ ];
119
+
120
+ if (result.findings.length > 0) {
121
+ lines.push(...result.findings.map((entry) =>
122
+ `- ${entry.severity.toUpperCase()} ${entry.ruleId} ${entry.path}: ${entry.message}`
123
+ ));
124
+ }
125
+
126
+ return lines.join("\n");
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @param {object} params
133
+ * @param {string} params.message
134
+ * @param {string} params.path
135
+ * @param {string} params.ruleId
136
+ * @param {"error" | "warning"} params.severity
137
+ * @returns {{message: string, path: string, ruleId: string, severity: "error" | "warning"}}
138
+ */
139
+ function createFinding({message, path, ruleId, severity}) {
140
+ return {message, path, ruleId, severity};
141
+ }
142
+
143
+ /**
144
+ * Classifies a semantic-skin filename and derives its canonical agent document.
145
+ *
146
+ * @param {string} file
147
+ * @returns {{body: string, language: string | null, markerError: string | null} | null}
148
+ */
149
+ function parseSkinFilename(file) {
150
+ const explicitMatch = file.match(EXPLICIT_SKIN_FILENAME);
151
+ if (explicitMatch) {
152
+ return {body: `${explicitMatch[1]}.md`, language: explicitMatch[2], markerError: null};
153
+ }
154
+
155
+ const legacyMatch = file.match(LEGACY_SKIN_FILENAME);
156
+ if (legacyMatch) {
157
+ return {
158
+ body: `${legacyMatch[1]}.md`,
159
+ language: null,
160
+ markerError: `Skin language marker is required: ${file} must use <name>.skin.<lang>.md`,
161
+ };
162
+ }
163
+
164
+ const malformedMatch = file.match(MALFORMED_SKIN_FILENAME);
165
+ if (malformedMatch) {
166
+ return {
167
+ body: `${malformedMatch[1]}.md`,
168
+ language: null,
169
+ markerError: `Invalid skin language marker: ${file} must use a lowercase ISO 639-1 code`,
170
+ };
171
+ }
172
+
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * @param {object} params
178
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Constants} params.constants
179
+ * @param {typeof import("node:fs")} params.fs
180
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Path} params.pathService
181
+ * @param {string} params.projectRoot
182
+ * @returns {{
183
+ * ok: false,
184
+ * finding: {message: string, path: string, ruleId: string, severity: "error" | "warning"}
185
+ * } | {
186
+ * ok: true,
187
+ * metadata: {schema: string, version: string, initializedAt?: string}
188
+ * }}
189
+ */
190
+ function readMetadata({constants, fs, pathService, projectRoot}) {
191
+ const metadataPath = pathService.join(projectRoot, "ctx", "adsm.json");
192
+ if (!fs.existsSync(metadataPath)) {
193
+ return {
194
+ ok: false,
195
+ finding: createFinding({
196
+ message: "Missing ctx/adsm.json",
197
+ path: "ctx/adsm.json",
198
+ ruleId: "metadata-file",
199
+ severity: "error",
200
+ }),
201
+ };
202
+ }
203
+
204
+ try {
205
+ const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
206
+ if (parsed.schema !== constants.CONTEXT_SCHEMA) {
207
+ return {
208
+ ok: false,
209
+ finding: createFinding({
210
+ message: `Unexpected schema: ${parsed.schema}`,
211
+ path: "ctx/adsm.json",
212
+ ruleId: "metadata-schema",
213
+ severity: "error",
214
+ }),
215
+ };
216
+ }
217
+ if (!parsed.version) {
218
+ return {
219
+ ok: false,
220
+ finding: createFinding({
221
+ message: "Missing metadata version",
222
+ path: "ctx/adsm.json",
223
+ ruleId: "metadata-version",
224
+ severity: "error",
225
+ }),
226
+ };
227
+ }
228
+ return {ok: true, metadata: parsed};
229
+ } catch (error) {
230
+ return {
231
+ ok: false,
232
+ finding: createFinding({
233
+ message: `Invalid metadata JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
234
+ path: "ctx/adsm.json",
235
+ ruleId: "metadata-json",
236
+ severity: "error",
237
+ }),
238
+ };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * @param {object} params
244
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Constants} params.constants
245
+ * @param {typeof import("node:fs")} params.fs
246
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Path} params.pathService
247
+ * @param {string} params.projectRoot
248
+ * @returns {{
249
+ * findings: {
250
+ * message: string,
251
+ * path: string,
252
+ * ruleId: string,
253
+ * severity: "error" | "warning"
254
+ * }[],
255
+ * summary: {
256
+ * assets: string[],
257
+ * bodies: string[],
258
+ * orphanSkins: string[],
259
+ * pairs: {body: string, skin: string}[],
260
+ * skins: string[]
261
+ * }
262
+ * }}
263
+ */
264
+ function inspectProjection({constants, fs, pathService, projectRoot}) {
265
+ const docsRoot = pathService.join(projectRoot, "ctx", "docs");
266
+ const assetsRoot = pathService.join(projectRoot, "ctx", "assets");
267
+ const findings = [];
268
+ const bodies = [];
269
+ const skins = [];
270
+ const assets = fs.existsSync(assetsRoot) ? listFiles({fs, pathService, root: assetsRoot, prefix: "ctx/assets"}) : [];
271
+
272
+ if (!fs.existsSync(docsRoot)) {
273
+ return {findings, summary: {assets, bodies, orphanSkins: [], pairs: [], skins: []}};
274
+ }
275
+
276
+ let docsEntries = [];
277
+ try {
278
+ docsEntries = listEntries({fs, root: docsRoot});
279
+ } catch (error) {
280
+ findings.push(createFinding({
281
+ message: `Unreadable documentation root (${error instanceof Error ? error.message : "Unknown error"})`,
282
+ path: "ctx/docs",
283
+ ruleId: "docs-root-readable",
284
+ severity: "error",
285
+ }));
286
+ return {findings, summary: {assets, bodies, orphanSkins: [], pairs: [], skins: []}};
287
+ }
288
+ const docsEntryMap = new Map(docsEntries.map((entry) => [entry.name, entry]));
289
+ for (const level of constants.DOC_LEVELS) {
290
+ const entry = docsEntryMap.get(level);
291
+ if (!entry) {
292
+ findings.push(createFinding({
293
+ message: `Missing documentation level: ctx/docs/${level}`,
294
+ path: `ctx/docs/${level}`,
295
+ ruleId: "doc-level",
296
+ severity: "error",
297
+ }));
298
+ } else if (!entry.isDirectory()) {
299
+ findings.push(createFinding({
300
+ message: `Documentation level is not a directory: ctx/docs/${level}`,
301
+ path: `ctx/docs/${level}`,
302
+ ruleId: "doc-level",
303
+ severity: "error",
304
+ }));
305
+ }
306
+ }
307
+ findings.push(...validateDocumentationLevelCapacity({
308
+ constants,
309
+ docsEntryMap,
310
+ fs,
311
+ pathService,
312
+ projectRoot,
313
+ }));
314
+
315
+ const markdownFiles = [];
316
+ for (const file of listFiles({fs, pathService, root: docsRoot, prefix: "ctx/docs"})) {
317
+ if (!file.endsWith(".md")) continue;
318
+ markdownFiles.push(file);
319
+ if (file.endsWith("/AGENTS.md") || file === "ctx/docs/AGENTS.md") continue;
320
+ if (parseSkinFilename(file)) {
321
+ skins.push(file);
322
+ } else {
323
+ bodies.push(file);
324
+ }
325
+ }
326
+
327
+ const bodySet = new Set(bodies);
328
+ const orphanSkins = [];
329
+ const pairs = [];
330
+ for (const skin of skins) {
331
+ const skinInfo = parseSkinFilename(skin);
332
+ const body = skinInfo.body;
333
+ if (bodySet.has(body)) {
334
+ pairs.push({body, skin});
335
+ } else {
336
+ orphanSkins.push(skin);
337
+ findings.push(createFinding({
338
+ message: `Orphan semantic skin: ${skin}`,
339
+ path: skin,
340
+ ruleId: "skin-paired-agent-document",
341
+ severity: "error",
342
+ }));
343
+ }
344
+ }
345
+
346
+ for (const file of markdownFiles) {
347
+ const absolutePath = pathService.join(projectRoot, ...file.split("/"));
348
+ let text = "";
349
+ try {
350
+ text = fs.readFileSync(absolutePath, "utf8");
351
+ } catch (error) {
352
+ findings.push(createFinding({
353
+ message: `Unreadable document (${error instanceof Error ? error.message : "Unknown error"})`,
354
+ path: file,
355
+ ruleId: "document-readable",
356
+ severity: "error",
357
+ }));
358
+ continue;
359
+ }
360
+ findings.push(...validateDocumentMetadata({file, text}));
361
+ findings.push(...validateLocalMarkdownLinks({file, fs, pathService, projectRoot, text}));
362
+ const skinInfo = parseSkinFilename(file);
363
+ if (skinInfo) {
364
+ findings.push(...validateSkinDocument({bodySet, constants, file, skinInfo, text}));
365
+ }
366
+ }
367
+
368
+ return {
369
+ findings,
370
+ summary: {
371
+ assets,
372
+ bodies,
373
+ orphanSkins,
374
+ pairs,
375
+ skins,
376
+ },
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Warns when a documentation level exceeds the recommended direct-entry capacity
382
+ * and requires its local AGENTS.md to make an intentional exception explicit.
383
+ *
384
+ * @param {object} params
385
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Constants} params.constants
386
+ * @param {Map<string, {name: string, isDirectory: () => boolean, isFile: () => boolean}>} params.docsEntryMap
387
+ * @param {typeof import("node:fs")} params.fs
388
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Path} params.pathService
389
+ * @param {string} params.projectRoot
390
+ * @returns {{message: string, path: string, ruleId: string, severity: "error" | "warning"}[]}
391
+ */
392
+ function validateDocumentationLevelCapacity({constants, docsEntryMap, fs, pathService, projectRoot}) {
393
+ const findings = [];
394
+ for (const level of constants.DOC_LEVELS) {
395
+ const levelEntry = docsEntryMap.get(level);
396
+ if (!levelEntry?.isDirectory()) continue;
397
+
398
+ const levelPath = `ctx/docs/${level}`;
399
+ let entries = [];
400
+ try {
401
+ entries = listEntries({fs, root: pathService.join(projectRoot, ...levelPath.split("/"))});
402
+ } catch (error) {
403
+ findings.push(createFinding({
404
+ message: `Unreadable documentation level (${error instanceof Error ? error.message : "Unknown error"})`,
405
+ path: levelPath,
406
+ ruleId: "doc-level-capacity",
407
+ severity: "error",
408
+ }));
409
+ continue;
410
+ }
411
+
412
+ const directoryCount = entries.filter((entry) => entry.isDirectory()).length;
413
+ const fileCount = entries.filter((entry) => entry.isFile() && entry.name !== "AGENTS.md" && !parseSkinFilename(entry.name)).length;
414
+ const exceeded = [];
415
+ if (directoryCount > constants.DOC_LEVEL_MAX_DIRECTORIES) {
416
+ exceeded.push(`${directoryCount} directories (recommended maximum ${constants.DOC_LEVEL_MAX_DIRECTORIES})`);
417
+ }
418
+ if (fileCount > constants.DOC_LEVEL_MAX_FILES) {
419
+ exceeded.push(`${fileCount} files (recommended maximum ${constants.DOC_LEVEL_MAX_FILES})`);
420
+ }
421
+ if (exceeded.length === 0) continue;
422
+
423
+ findings.push(createFinding({
424
+ message: `Documentation level exceeds recommended capacity: ${exceeded.join(", ")}`,
425
+ path: levelPath,
426
+ ruleId: "doc-level-capacity",
427
+ severity: "warning",
428
+ }));
429
+
430
+ const agentsPath = pathService.join(projectRoot, ...levelPath.split("/"), "AGENTS.md");
431
+ let agentsText = "";
432
+ try {
433
+ agentsText = fs.readFileSync(agentsPath, "utf8");
434
+ } catch (error) {
435
+ findings.push(createFinding({
436
+ message: `Missing readable Documentation Capacity Exception in ${levelPath}/AGENTS.md`,
437
+ path: `${levelPath}/AGENTS.md`,
438
+ ruleId: "doc-level-capacity-exception",
439
+ severity: "error",
440
+ }));
441
+ continue;
442
+ }
443
+ if (extractSectionBody({sectionName: "Documentation Capacity Exception", text: agentsText}).trim().length === 0) {
444
+ findings.push(createFinding({
445
+ message: `Documentation Capacity Exception must be explicit in ${levelPath}/AGENTS.md`,
446
+ path: `${levelPath}/AGENTS.md`,
447
+ ruleId: "doc-level-capacity-exception",
448
+ severity: "error",
449
+ }));
450
+ }
451
+ }
452
+ return findings;
453
+ }
454
+
455
+ /**
456
+ * @param {object} params
457
+ * @param {typeof import("node:fs")} params.fs
458
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Path} params.pathService
459
+ * @param {string} params.root
460
+ * @param {string} params.prefix
461
+ * @returns {string[]}
462
+ */
463
+ function listFiles({fs, pathService, root, prefix}) {
464
+ const result = [];
465
+ for (const entry of listEntries({fs, root})) {
466
+ const childPath = pathService.join(root, entry.name);
467
+ const childPrefix = `${prefix}/${entry.name}`;
468
+ if (entry.isDirectory()) {
469
+ result.push(...listFiles({fs, pathService, root: childPath, prefix: childPrefix}));
470
+ } else if (entry.isFile()) {
471
+ result.push(childPrefix);
472
+ }
473
+ }
474
+ return result.sort();
475
+ }
476
+
477
+ /**
478
+ * @param {object} params
479
+ * @param {typeof import("node:fs")} params.fs
480
+ * @param {string} params.root
481
+ * @returns {{name: string, isDirectory: () => boolean, isFile: () => boolean}[]}
482
+ */
483
+ function listEntries({fs, root}) {
484
+ return fs.readdirSync(root, {withFileTypes: true});
485
+ }
486
+
487
+ /**
488
+ * @param {object} params
489
+ * @param {string} params.file
490
+ * @param {{body: string, language: string | null, markerError: string | null}} params.skinInfo
491
+ * @param {string} params.text
492
+ * @returns {{message: string, path: string, ruleId: string, severity: "error" | "warning"}[]}
493
+ */
494
+ function validateDocumentMetadata({file, text}) {
495
+ const findings = [];
496
+ const pathMatch = text.match(/^- Path: `([^`]+)`$/m);
497
+ if (!pathMatch) {
498
+ findings.push(createFinding({
499
+ message: `Missing Path attribute: ${file}`,
500
+ path: file,
501
+ ruleId: "doc-header-path-changed",
502
+ severity: "error",
503
+ }));
504
+ } else if (pathMatch[1] !== file) {
505
+ findings.push(createFinding({
506
+ message: `Path mismatch: ${file} declares ${pathMatch[1]}`,
507
+ path: file,
508
+ ruleId: "doc-header-path-changed",
509
+ severity: "error",
510
+ }));
511
+ }
512
+
513
+ const changedMatch = text.match(/^- Changed: `([^`]+)`$/m);
514
+ if (!changedMatch) {
515
+ findings.push(createFinding({
516
+ message: `Missing Changed attribute: ${file}`,
517
+ path: file,
518
+ ruleId: "doc-header-path-changed",
519
+ severity: "error",
520
+ }));
521
+ } else if (!/^\d{8}$/.test(changedMatch[1])) {
522
+ findings.push(createFinding({
523
+ message: `Invalid Changed attribute: ${file} declares ${changedMatch[1]}`,
524
+ path: file,
525
+ ruleId: "doc-header-path-changed",
526
+ severity: "error",
527
+ }));
528
+ }
529
+
530
+ return findings;
531
+ }
532
+
533
+ /**
534
+ * @param {object} params
535
+ * @param {string} params.file
536
+ * @param {typeof import("node:fs")} params.fs
537
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Path} params.pathService
538
+ * @param {string} params.projectRoot
539
+ * @param {string} params.text
540
+ * @returns {{message: string, path: string, ruleId: string, severity: "error" | "warning"}[]}
541
+ */
542
+ function validateLocalMarkdownLinks({file, fs, pathService, projectRoot, text}) {
543
+ const findings = [];
544
+ for (const [, rawLink] of text.matchAll(LINK_PATTERN)) {
545
+ const normalized = normalizeLocalMarkdownLink({file, link: rawLink});
546
+ if (!normalized) continue;
547
+
548
+ const resolvedPath = pathService.join(projectRoot, ...normalized.path.split("/"));
549
+ if (!fs.existsSync(resolvedPath)) {
550
+ findings.push(createFinding({
551
+ message: `Missing linked document: ${rawLink} resolves to ${normalized.path}`,
552
+ path: file,
553
+ ruleId: "local-link-exists",
554
+ severity: "error",
555
+ }));
556
+ continue;
557
+ }
558
+
559
+ if (normalized.fragment) {
560
+ findings.push(createFinding({
561
+ message: `Link anchor was not validated: ${rawLink}`,
562
+ path: file,
563
+ ruleId: "local-link-exists",
564
+ severity: "warning",
565
+ }));
566
+ }
567
+ }
568
+ return findings;
569
+ }
570
+
571
+ /**
572
+ * @param {object} params
573
+ * @param {Set<string>} params.bodySet
574
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Constants} params.constants
575
+ * @param {string} params.file
576
+ * @param {string} params.text
577
+ * @returns {{message: string, path: string, ruleId: string, severity: "error" | "warning"}[]}
578
+ */
579
+ function validateSkinDocument({bodySet, constants, file, skinInfo, text}) {
580
+ const findings = [];
581
+ if (skinInfo.markerError) {
582
+ findings.push(createFinding({
583
+ message: skinInfo.markerError,
584
+ path: file,
585
+ ruleId: "skin-language-marker",
586
+ severity: "error",
587
+ }));
588
+ }
589
+ const headings = extractLevelTwoHeadings(text);
590
+ for (const section of REQUIRED_SKIN_SECTIONS) {
591
+ if (!headings.includes(section)) {
592
+ findings.push(createFinding({
593
+ message: `Missing skin section "${section}": ${file}`,
594
+ path: file,
595
+ ruleId: "skin-required-sections",
596
+ severity: "error",
597
+ }));
598
+ }
599
+ }
600
+
601
+ const scopeText = extractSectionBody({sectionName: "Scope", text});
602
+ if (scopeText) {
603
+ if (!/^Includes:\s*$/m.test(scopeText)) {
604
+ findings.push(createFinding({
605
+ message: `Missing Includes: block in Scope: ${file}`,
606
+ path: file,
607
+ ruleId: "skin-scope-includes-excludes",
608
+ severity: "error",
609
+ }));
610
+ }
611
+ if (!/^Excludes:\s*$/m.test(scopeText)) {
612
+ findings.push(createFinding({
613
+ message: `Missing Excludes: block in Scope: ${file}`,
614
+ path: file,
615
+ ruleId: "skin-scope-includes-excludes",
616
+ severity: "error",
617
+ }));
618
+ }
619
+ }
620
+
621
+ const declaredReference = extractAgentDocumentReference(text);
622
+ if (!declaredReference) {
623
+ findings.push(createFinding({
624
+ message: `Missing Agent Document reference: ${file}`,
625
+ path: file,
626
+ ruleId: "skin-agent-document-reference",
627
+ severity: "error",
628
+ }));
629
+ findings.push(...validateSkinMetrics({constants, file, text}));
630
+ return findings;
631
+ }
632
+
633
+ const expectedReference = skinInfo.body;
634
+ const normalizedReference = normalizeAgentDocumentReference({reference: declaredReference, skinFile: file});
635
+ if (normalizedReference !== expectedReference) {
636
+ findings.push(createFinding({
637
+ message: `Skin agent document mismatch: ${file} references ${normalizedReference} but expected ${expectedReference}`,
638
+ path: file,
639
+ ruleId: "skin-agent-document-reference",
640
+ severity: "error",
641
+ }));
642
+ }
643
+ if (!bodySet.has(normalizedReference)) {
644
+ findings.push(createFinding({
645
+ message: `Missing paired agent document: ${file} references ${normalizedReference}`,
646
+ path: file,
647
+ ruleId: "skin-paired-agent-document",
648
+ severity: "error",
649
+ }));
650
+ }
651
+
652
+ findings.push(...validateSkinMetrics({constants, file, text}));
653
+ return findings;
654
+ }
655
+
656
+ /**
657
+ * @param {object} params
658
+ * @param {Fl32_Skill_Adsm_Ctx_Shared_Constants} params.constants
659
+ * @param {string} params.file
660
+ * @param {string} params.text
661
+ * @returns {{message: string, path: string, ruleId: string, severity: "error" | "warning"}[]}
662
+ */
663
+ function validateSkinMetrics({constants, file, text}) {
664
+ const findings = [];
665
+ const metricText = extractSkinMetricText(text);
666
+ const wordCount = countWords(metricText);
667
+ const listCount = countMeaningfulListItems(metricText);
668
+
669
+ if (wordCount > constants.SKIN_WORD_ERROR) {
670
+ findings.push(createFinding({
671
+ message: `Skin word count ${wordCount} exceeds limit ${constants.SKIN_WORD_ERROR}`,
672
+ path: file,
673
+ ruleId: "skin-size-word-count",
674
+ severity: "error",
675
+ }));
676
+ } else if (wordCount > constants.SKIN_WORD_WARN) {
677
+ findings.push(createFinding({
678
+ message: `Skin word count ${wordCount} exceeds warning threshold ${constants.SKIN_WORD_WARN}`,
679
+ path: file,
680
+ ruleId: "skin-size-word-count",
681
+ severity: "warning",
682
+ }));
683
+ }
684
+
685
+ if (listCount > constants.SKIN_LIST_ERROR) {
686
+ findings.push(createFinding({
687
+ message: `Skin list count ${listCount} exceeds limit ${constants.SKIN_LIST_ERROR}`,
688
+ path: file,
689
+ ruleId: "skin-size-list-count",
690
+ severity: "error",
691
+ }));
692
+ } else if (listCount > constants.SKIN_LIST_WARN) {
693
+ findings.push(createFinding({
694
+ message: `Skin list count ${listCount} exceeds warning threshold ${constants.SKIN_LIST_WARN}`,
695
+ path: file,
696
+ ruleId: "skin-size-list-count",
697
+ severity: "warning",
698
+ }));
699
+ }
700
+
701
+ return findings;
702
+ }
703
+
704
+ /**
705
+ * @param {string} text
706
+ * @returns {string[]}
707
+ */
708
+ function extractLevelTwoHeadings(text) {
709
+ return [...text.matchAll(/^##\s+(.+)$/gm)].map((match) => match[1].trim());
710
+ }
711
+
712
+ /**
713
+ * @param {object} params
714
+ * @param {string} params.sectionName
715
+ * @param {string} params.text
716
+ * @returns {string}
717
+ */
718
+ function extractSectionBody({sectionName, text}) {
719
+ const lines = text.split("\n");
720
+ const body = [];
721
+ let insideSection = false;
722
+ for (const rawLine of lines) {
723
+ const line = rawLine.trim();
724
+ if (!insideSection) {
725
+ if (line === `## ${sectionName}`) insideSection = true;
726
+ continue;
727
+ }
728
+ if (line.startsWith("## ")) break;
729
+ body.push(rawLine);
730
+ }
731
+ return body.join("\n");
732
+ }
733
+
734
+ /**
735
+ * @param {string} text
736
+ * @returns {string | null}
737
+ */
738
+ function extractAgentDocumentReference(text) {
739
+ const lines = text.split("\n");
740
+ let insideSection = false;
741
+ for (const rawLine of lines) {
742
+ const line = rawLine.trim();
743
+ if (!insideSection) {
744
+ if (line === "## Agent Document") insideSection = true;
745
+ continue;
746
+ }
747
+ if (line.startsWith("## ")) break;
748
+ if (line.length > 0) return line;
749
+ }
750
+ return null;
751
+ }
752
+
753
+ /**
754
+ * @param {object} params
755
+ * @param {string} params.reference
756
+ * @param {string} params.skinFile
757
+ * @returns {string}
758
+ */
759
+ function normalizeAgentDocumentReference({reference, skinFile}) {
760
+ let value = reference.trim();
761
+ const codeMatch = value.match(/^`(.+)`$/);
762
+ if (codeMatch) value = codeMatch[1].trim();
763
+ const linkMatch = value.match(/^\[[^\]]+\]\(([^)]+)\)$/);
764
+ if (linkMatch) value = linkMatch[1].trim();
765
+ if (value.startsWith("./")) value = value.slice(2);
766
+ if (value.startsWith("ctx/docs/")) return value;
767
+
768
+ const skinParts = skinFile.split("/");
769
+ skinParts.pop();
770
+ skinParts.push(value);
771
+ return normalizeRelativeSegments(skinParts).join("/");
772
+ }
773
+
774
+ /**
775
+ * @param {object} params
776
+ * @param {string} params.file
777
+ * @param {string} params.link
778
+ * @returns {{fragment: string | null, path: string} | null}
779
+ */
780
+ function normalizeLocalMarkdownLink({file, link}) {
781
+ const trimmed = link.trim();
782
+ if (!trimmed) return null;
783
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("mailto:")) return null;
784
+
785
+ const [rawPath, fragment = ""] = trimmed.split("#");
786
+ if (!rawPath.endsWith(".md")) return null;
787
+
788
+ let path = rawPath;
789
+ if (path.startsWith("./")) path = path.slice(2);
790
+
791
+ if (path.startsWith("ctx/docs/")) {
792
+ return {fragment: fragment || null, path: normalizeRelativeSegments(path.split("/")).join("/")};
793
+ }
794
+
795
+ const parts = file.split("/");
796
+ parts.pop();
797
+ parts.push(...path.split("/"));
798
+ return {
799
+ fragment: fragment || null,
800
+ path: normalizeRelativeSegments(parts).join("/"),
801
+ };
802
+ }
803
+
804
+ /**
805
+ * @param {string[]} parts
806
+ * @returns {string[]}
807
+ */
808
+ function normalizeRelativeSegments(parts) {
809
+ const result = [];
810
+ for (const part of parts) {
811
+ if (!part || part === ".") continue;
812
+ if (part === "..") {
813
+ if (result.length > 0) result.pop();
814
+ continue;
815
+ }
816
+ result.push(part);
817
+ }
818
+ return result;
819
+ }
820
+
821
+ /**
822
+ * @param {string} text
823
+ * @returns {string}
824
+ */
825
+ function stripFencedCodeBlocks(text) {
826
+ return text.replace(/```[\s\S]*?```/g, "");
827
+ }
828
+
829
+ /**
830
+ * Extracts semantic skin content while excluding metadata, headings, and the
831
+ * Agent Document reference.
832
+ *
833
+ * @param {string} text
834
+ * @returns {string}
835
+ */
836
+ function extractSkinMetricText(text) {
837
+ return ["Purpose", "Mental Model", "Scope", "Invariants"]
838
+ .map((sectionName) => extractSectionBody({sectionName, text}))
839
+ .join("\n")
840
+ .replace(/^\s{0,3}#{1,6}\s+.*$/gm, "")
841
+ .replace(/^\s*(?:Includes|Excludes):\s*$/gm, "");
842
+ }
843
+
844
+ /**
845
+ * @param {string} text
846
+ * @returns {string}
847
+ */
848
+ function stripSkinPresentation(text) {
849
+ return stripFencedCodeBlocks(text)
850
+ .split("\n")
851
+ .filter((line) => !/^#/.test(line.trim()))
852
+ .filter((line) => !/^- (?:Path|Changed): `[^`]+`$/.test(line.trim()))
853
+ .join("\n");
854
+ }
855
+
856
+ /**
857
+ * @param {string} text
858
+ * @returns {number}
859
+ */
860
+ function countWords(text) {
861
+ const matches = stripFencedCodeBlocks(text).match(/(?:\p{L}[\p{L}\p{M}\p{N}'’_-]*|\p{N}+)/gu);
862
+ return matches ? matches.length : 0;
863
+ }
864
+
865
+ /**
866
+ * @param {string} text
867
+ * @returns {number}
868
+ */
869
+ function countMeaningfulListItems(text) {
870
+ return [...stripFencedCodeBlocks(text).matchAll(/^\s*(?:[-*+]|\d+[.)])\s+(.+)$/gmu)]
871
+ .filter((match) => countWords(match[1]) > 0)
872
+ .length;
873
+ }
874
+
875
+ export const __deps__ = Object.freeze({
876
+ constants: "Fl32_Skill_Adsm_Ctx_Shared_Constants$",
877
+ fs: "node:fs",
878
+ pathService: "Fl32_Skill_Adsm_Ctx_Shared_Path$",
879
+ });