@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.
- package/.github/workflows/flows-app.yaml +95 -0
- package/AGENTS.md +112 -0
- package/LICENSE +201 -0
- package/README.md +246 -0
- package/bin/cli.mjs +55 -0
- package/ctx/AGENTS.md +44 -0
- package/ctx/README.md +24 -0
- package/ctx/adsm.json +5 -0
- package/ctx/agent/AGENTS.md +41 -0
- package/ctx/agent/flows/.img/flancer32-skill-adsm-ctx-github-flows.png +0 -0
- package/ctx/agent/flows/AGENTS.md +37 -0
- package/ctx/agent/flows/cfg/issues/labeled/accepted/profile.json +25 -0
- package/ctx/agent/flows/cfg/issues/labeled/accepted/prompt.md +104 -0
- package/ctx/agent/flows/cfg/issues/labeled/clarification-provided/profile.json +15 -0
- package/ctx/agent/flows/cfg/issues/labeled/clarification-provided/prompt.md +63 -0
- package/ctx/agent/flows/cfg/issues/opened/admission/profile.json +14 -0
- package/ctx/agent/flows/cfg/issues/opened/admission/prompt.md +110 -0
- package/ctx/agent/flows/cfg/profile.json +40 -0
- package/ctx/agent/flows/cfg/pull_request/closed/merged/profile.json +15 -0
- package/ctx/agent/flows/cfg/pull_request/closed/merged/prompt.md +41 -0
- package/ctx/agent/flows/cfg/pull_request/labeled/needs-fix/profile.json +25 -0
- package/ctx/agent/flows/cfg/pull_request/labeled/needs-fix/prompt.md +79 -0
- package/ctx/agent/flows/cfg/pull_request/labeled/ready-for-validation/profile.json +15 -0
- package/ctx/agent/flows/cfg/pull_request/labeled/ready-for-validation/prompt.md +72 -0
- package/ctx/agent/flows/cfg/pull_request/labeled/verified/profile.json +15 -0
- package/ctx/agent/flows/cfg/pull_request/labeled/verified/prompt.md +48 -0
- package/ctx/agent/flows/deployment.md +116 -0
- package/ctx/agent/flows/labels.md +108 -0
- package/ctx/agent/flows/overview.md +82 -0
- package/ctx/agent/prompt/cli/ai-intro.md +141 -0
- package/ctx/assets/.gitkeep +1 -0
- package/ctx/docs/AGENTS.md +121 -0
- package/ctx/docs/ai-intro.md +37 -0
- package/ctx/docs/architecture/AGENTS.md +98 -0
- package/ctx/docs/architecture/behavior.md +68 -0
- package/ctx/docs/architecture/checklists.md +48 -0
- package/ctx/docs/architecture/constraints.md +53 -0
- package/ctx/docs/architecture/constraints.skin.en.md +38 -0
- package/ctx/docs/architecture/decisions.md +118 -0
- package/ctx/docs/architecture/integration.md +31 -0
- package/ctx/docs/architecture/overview.md +76 -0
- package/ctx/docs/architecture/state.md +50 -0
- package/ctx/docs/architecture/structure.md +38 -0
- package/ctx/docs/architecture/supervision.md +71 -0
- package/ctx/docs/code/AGENTS.md +51 -0
- package/ctx/docs/code/overview.md +53 -0
- package/ctx/docs/code/testing.md +26 -0
- package/ctx/docs/environment/AGENTS.md +50 -0
- package/ctx/docs/environment/overview.md +33 -0
- package/ctx/docs/filesystem.md +44 -0
- package/ctx/docs/product/AGENTS.md +51 -0
- package/ctx/docs/product/domain.md +63 -0
- package/ctx/docs/product/domain.skin.en.md +40 -0
- package/ctx/docs/product/glossary.md +31 -0
- package/ctx/docs/product/overview.md +112 -0
- package/ctx/docs/product/overview.skin.en.md +38 -0
- package/ctx/docs/product/roles.md +17 -0
- package/ctx/docs/product/use-cases.md +15 -0
- package/jsconfig.json +12 -0
- package/migrations/README.md +9 -0
- package/package.json +33 -0
- package/skill/SKILL.md +47 -0
- package/skill/references/agents-hierarchy.md +45 -0
- package/skill/references/context-lifecycle.md +53 -0
- package/skill/references/design-questionnaire.md +44 -0
- package/skill/references/document-projections.md +290 -0
- package/skill/references/documentation-compression.md +117 -0
- package/skill/references/documentation-quality-audit.md +199 -0
- package/skill/references/orchestration-model.md +41 -0
- package/skill/references/specialized-skills.md +31 -0
- package/skill/references/upgrade-policy.md +19 -0
- package/skill/templates/AGENTS.md +120 -0
- package/skill/templates/ai/AGENTS.md +59 -0
- package/skill/templates/ctx/AGENTS.md +45 -0
- package/skill/templates/ctx/README.md +27 -0
- package/skill/templates/ctx/adsm.json +7 -0
- package/skill/templates/ctx/agent/AGENTS.md +36 -0
- package/skill/templates/ctx/assets/.gitkeep +1 -0
- package/skill/templates/ctx/docs/AGENTS.md +55 -0
- package/skill/templates/ctx/docs/ai-intro.md +53 -0
- package/skill/templates/ctx/docs/architecture/AGENTS.md +42 -0
- package/skill/templates/ctx/docs/architecture/behavior.md +38 -0
- package/skill/templates/ctx/docs/architecture/checklists.md +25 -0
- package/skill/templates/ctx/docs/architecture/constraints.md +36 -0
- package/skill/templates/ctx/docs/architecture/decisions.md +33 -0
- package/skill/templates/ctx/docs/architecture/integration.md +27 -0
- package/skill/templates/ctx/docs/architecture/overview.md +73 -0
- package/skill/templates/ctx/docs/architecture/state.md +52 -0
- package/skill/templates/ctx/docs/architecture/structure.md +32 -0
- package/skill/templates/ctx/docs/architecture/supervision.md +61 -0
- package/skill/templates/ctx/docs/code/AGENTS.md +35 -0
- package/skill/templates/ctx/docs/code/overview.md +21 -0
- package/skill/templates/ctx/docs/code/testing.md +13 -0
- package/skill/templates/ctx/docs/environment/AGENTS.md +34 -0
- package/skill/templates/ctx/docs/environment/overview.md +21 -0
- package/skill/templates/ctx/docs/filesystem.md +39 -0
- package/skill/templates/ctx/docs/product/AGENTS.md +38 -0
- package/skill/templates/ctx/docs/product/domain.md +77 -0
- package/skill/templates/ctx/docs/product/glossary.md +110 -0
- package/skill/templates/ctx/docs/product/overview.md +74 -0
- package/skill/templates/ctx/docs/product/roles.md +100 -0
- package/skill/templates/ctx/docs/product/use-cases.md +109 -0
- package/skill/templates/work/context/semantic-skin.md +27 -0
- package/skill/templates/work/report/documentation-compression.md +34 -0
- package/skill/templates/work/report/documentation-quality-audit.md +90 -0
- package/src/Bootstrap/Cli.mjs +62 -0
- package/src/Bootstrap.mjs +108 -0
- package/src/Design/Service.mjs +36 -0
- package/src/Init/Service.mjs +136 -0
- package/src/Shared/Constants.mjs +36 -0
- package/src/Shared/CopyDirectory.mjs +78 -0
- package/src/Shared/Metadata.mjs +30 -0
- package/src/Shared/Path.mjs +38 -0
- package/src/Skill/Installer.mjs +47 -0
- package/src/Upgrade/Service.mjs +44 -0
- package/src/Validate/Service.mjs +879 -0
- package/test/integration/.gitkeep +1 -0
- package/test/integration/cli-help.test.mjs +15 -0
- package/test/integration/cli-init-validate.test.mjs +223 -0
- package/test/integration/cli-install-skill.test.mjs +41 -0
- package/test/unit/Bootstrap/Cli.test.mjs +23 -0
- package/test/unit/Bootstrap.test.mjs +53 -0
- package/test/unit/Design/Service.test.mjs +11 -0
- package/test/unit/Init/Service.test.mjs +51 -0
- package/test/unit/Shared/Constants.test.mjs +21 -0
- package/test/unit/Shared/CopyDirectory.test.mjs +31 -0
- package/test/unit/Shared/Metadata.test.mjs +9 -0
- package/test/unit/Shared/Path.test.mjs +11 -0
- package/test/unit/Skill/Installer.test.mjs +20 -0
- package/test/unit/Upgrade/Service.test.mjs +13 -0
- package/test/unit/Validate/Service.test.mjs +760 -0
- 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
|
+
});
|