@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,760 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import Service from '../../../src/Validate/Service.mjs';
|
|
5
|
+
|
|
6
|
+
const constants = {
|
|
7
|
+
CONTEXT_SCHEMA: 'adsm-context',
|
|
8
|
+
DOC_LEVELS: ['product', 'architecture', 'environment', 'code'],
|
|
9
|
+
REQUIRED_PATHS: ['ctx', 'ctx/docs', 'ctx/adsm.json'],
|
|
10
|
+
SKIN_WORD_WARN: 160,
|
|
11
|
+
SKIN_WORD_ERROR: 240,
|
|
12
|
+
SKIN_LIST_WARN: 12,
|
|
13
|
+
SKIN_LIST_ERROR: 20,
|
|
14
|
+
DOC_LEVEL_MAX_DIRECTORIES: 9,
|
|
15
|
+
DOC_LEVEL_MAX_FILES: 9,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
test('Validate service returns ok for matching metadata', async () => {
|
|
19
|
+
const fs = {
|
|
20
|
+
existsSync: (path) => [
|
|
21
|
+
'/project/ctx',
|
|
22
|
+
'/project/ctx/docs',
|
|
23
|
+
'/project/ctx/docs/product',
|
|
24
|
+
'/project/ctx/docs/architecture',
|
|
25
|
+
'/project/ctx/docs/environment',
|
|
26
|
+
'/project/ctx/docs/code',
|
|
27
|
+
'/project/ctx/adsm.json',
|
|
28
|
+
].includes(path),
|
|
29
|
+
readFileSync: (filePath) => filePath.endsWith('adsm.json') ? '{"schema":"adsm-context","version":"0.1.0"}' : '',
|
|
30
|
+
readdirSync: (dirPath) => {
|
|
31
|
+
if (dirPath === '/project/ctx/docs') return dirents(['product', 'architecture', 'environment', 'code'], 'dir');
|
|
32
|
+
return [];
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const service = new Service({
|
|
36
|
+
constants,
|
|
37
|
+
fs,
|
|
38
|
+
pathService: {join: (...parts) => parts.join('/'), resolve: (value) => value},
|
|
39
|
+
});
|
|
40
|
+
const result = await service.execute({targetPath: '/project'});
|
|
41
|
+
assert.equal(result.ok, true);
|
|
42
|
+
assert.equal(result.errorCount, 0);
|
|
43
|
+
assert.equal(result.warningCount, 0);
|
|
44
|
+
assert.equal(result.metadata.version, '0.1.0');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Validate service reports missing paths and schema mismatch as errors', async () => {
|
|
48
|
+
const fs = {
|
|
49
|
+
existsSync: (path) => path === '/project/ctx/adsm.json',
|
|
50
|
+
readFileSync: () => '{"schema":"wrong","version":"0.1.0"}',
|
|
51
|
+
readdirSync: () => [],
|
|
52
|
+
};
|
|
53
|
+
const service = new Service({
|
|
54
|
+
constants,
|
|
55
|
+
fs,
|
|
56
|
+
pathService: {join: (...parts) => parts.join('/'), resolve: (value) => value},
|
|
57
|
+
});
|
|
58
|
+
const result = await service.execute({targetPath: '/project'});
|
|
59
|
+
assert.equal(result.ok, false);
|
|
60
|
+
assert.match(formatFindings(result.findings), /Missing: ctx/);
|
|
61
|
+
assert.match(formatFindings(result.findings), /Unexpected schema: wrong/);
|
|
62
|
+
assert.equal(result.findings.every((entry) => entry.severity === 'error'), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('Validate service recognizes body and skin pairs recursively', async () => {
|
|
66
|
+
const files = {
|
|
67
|
+
'/project/ctx/docs/product/AGENTS.md': doc({
|
|
68
|
+
path: 'ctx/docs/product/AGENTS.md',
|
|
69
|
+
body: '## Purpose\n\nProduct routing.\n',
|
|
70
|
+
}),
|
|
71
|
+
'/project/ctx/docs/product/domain/ingress.md': doc({
|
|
72
|
+
path: 'ctx/docs/product/domain/ingress.md',
|
|
73
|
+
body: '## Purpose\n\nIngress context.\n',
|
|
74
|
+
}),
|
|
75
|
+
'/project/ctx/docs/product/domain/ingress.skin.en.md': skin({
|
|
76
|
+
path: 'ctx/docs/product/domain/ingress.skin.en.md',
|
|
77
|
+
agentDocument: 'ingress.md',
|
|
78
|
+
}),
|
|
79
|
+
'/project/ctx/docs/product/domain/memory.skin.en.md': skin({
|
|
80
|
+
path: 'ctx/docs/product/domain/memory.skin.en.md',
|
|
81
|
+
agentDocument: 'memory.md',
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
const fs = {
|
|
85
|
+
existsSync: (filePath) => [
|
|
86
|
+
'/project/ctx',
|
|
87
|
+
'/project/ctx/docs',
|
|
88
|
+
'/project/ctx/docs/product',
|
|
89
|
+
'/project/ctx/docs/architecture',
|
|
90
|
+
'/project/ctx/docs/environment',
|
|
91
|
+
'/project/ctx/docs/code',
|
|
92
|
+
'/project/ctx/assets',
|
|
93
|
+
'/project/ctx/adsm.json',
|
|
94
|
+
].includes(filePath),
|
|
95
|
+
readFileSync: (filePath) => filePath.endsWith('adsm.json')
|
|
96
|
+
? '{"schema":"adsm-context","version":"0.1.0"}'
|
|
97
|
+
: files[filePath],
|
|
98
|
+
readdirSync: (dirPath) => {
|
|
99
|
+
const tree = {
|
|
100
|
+
'/project/ctx/docs': dirents(['product', 'architecture', 'environment', 'code'], 'dir'),
|
|
101
|
+
'/project/ctx/docs/product': [dirent('domain', 'dir'), dirent('AGENTS.md', 'file')],
|
|
102
|
+
'/project/ctx/docs/product/domain': [
|
|
103
|
+
dirent('ingress.md', 'file'),
|
|
104
|
+
dirent('ingress.skin.en.md', 'file'),
|
|
105
|
+
dirent('memory.skin.en.md', 'file'),
|
|
106
|
+
],
|
|
107
|
+
'/project/ctx/docs/architecture': [],
|
|
108
|
+
'/project/ctx/docs/environment': [],
|
|
109
|
+
'/project/ctx/docs/code': [],
|
|
110
|
+
'/project/ctx/assets': [dirent('product', 'dir')],
|
|
111
|
+
'/project/ctx/assets/product': [dirent('domain', 'dir')],
|
|
112
|
+
'/project/ctx/assets/product/domain': [dirent('main-map.svg', 'file')],
|
|
113
|
+
};
|
|
114
|
+
return tree[dirPath] ?? [];
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const service = new Service({
|
|
118
|
+
constants,
|
|
119
|
+
fs,
|
|
120
|
+
pathService: {join: (...parts) => parts.join('/'), resolve: (value) => value},
|
|
121
|
+
});
|
|
122
|
+
const result = await service.execute({targetPath: '/project'});
|
|
123
|
+
assert.equal(result.ok, false);
|
|
124
|
+
assert.deepEqual(result.projection.bodies, ['ctx/docs/product/domain/ingress.md']);
|
|
125
|
+
assert.deepEqual(result.projection.skins, [
|
|
126
|
+
'ctx/docs/product/domain/ingress.skin.en.md',
|
|
127
|
+
'ctx/docs/product/domain/memory.skin.en.md',
|
|
128
|
+
]);
|
|
129
|
+
assert.deepEqual(result.projection.pairs, [{
|
|
130
|
+
body: 'ctx/docs/product/domain/ingress.md',
|
|
131
|
+
skin: 'ctx/docs/product/domain/ingress.skin.en.md',
|
|
132
|
+
}]);
|
|
133
|
+
assert.deepEqual(result.projection.orphanSkins, ['ctx/docs/product/domain/memory.skin.en.md']);
|
|
134
|
+
assert.deepEqual(result.projection.assets, ['ctx/assets/product/domain/main-map.svg']);
|
|
135
|
+
assert.match(formatFindings(result.findings), /Orphan semantic skin: ctx\/docs\/product\/domain\/memory\.skin\.en\.md/);
|
|
136
|
+
assert.match(formatFindings(result.findings), /Missing paired agent document: ctx\/docs\/product\/domain\/memory\.skin\.en\.md references ctx\/docs\/product\/domain\/memory\.md/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('Validate service recognizes multiple language skins and rejects an unmarked skin', async () => {
|
|
140
|
+
const files = {
|
|
141
|
+
'/project/ctx/docs/product/domain.md': doc({path: 'ctx/docs/product/domain.md', body: 'Domain context.\n'}),
|
|
142
|
+
'/project/ctx/docs/product/domain.skin.en.md': skin({
|
|
143
|
+
agentDocument: 'domain.md',
|
|
144
|
+
path: 'ctx/docs/product/domain.skin.en.md',
|
|
145
|
+
}),
|
|
146
|
+
'/project/ctx/docs/product/domain.skin.ru.md': skin({
|
|
147
|
+
agentDocument: 'domain.md',
|
|
148
|
+
path: 'ctx/docs/product/domain.skin.ru.md',
|
|
149
|
+
}),
|
|
150
|
+
'/project/ctx/docs/product/overview.skin.md': skin({
|
|
151
|
+
agentDocument: 'overview.md',
|
|
152
|
+
path: 'ctx/docs/product/overview.skin.md',
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
const service = buildService({files});
|
|
156
|
+
const result = await service.execute({targetPath: '/project'});
|
|
157
|
+
|
|
158
|
+
assert.equal(result.projection.pairs.length, 2);
|
|
159
|
+
assert.deepEqual(result.projection.pairs.map((pair) => pair.skin), [
|
|
160
|
+
'ctx/docs/product/domain.skin.en.md',
|
|
161
|
+
'ctx/docs/product/domain.skin.ru.md',
|
|
162
|
+
]);
|
|
163
|
+
assert.match(formatFindings(result.findings), /skin-language-marker ctx\/docs\/product\/overview\.skin\.md Skin language marker is required/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('Validate service reports invalid metadata headers and skin structure', async () => {
|
|
167
|
+
const files = {
|
|
168
|
+
'/project/ctx/docs/product/AGENTS.md': doc({
|
|
169
|
+
path: 'ctx/docs/product/AGENTS.md',
|
|
170
|
+
body: '## Purpose\n\nProduct routing.\n',
|
|
171
|
+
}),
|
|
172
|
+
'/project/ctx/docs/product/domain.md': [
|
|
173
|
+
'# Product Domain',
|
|
174
|
+
'',
|
|
175
|
+
'- Path: `ctx/docs/product/wrong-domain.md`',
|
|
176
|
+
'',
|
|
177
|
+
'## Purpose',
|
|
178
|
+
'',
|
|
179
|
+
'Domain context.',
|
|
180
|
+
'',
|
|
181
|
+
].join('\n'),
|
|
182
|
+
'/project/ctx/docs/product/domain.skin.en.md': [
|
|
183
|
+
'# Product Domain Skin',
|
|
184
|
+
'',
|
|
185
|
+
'- Path: `ctx/docs/product/domain.skin.en.md`',
|
|
186
|
+
'- Changed: `20260708`',
|
|
187
|
+
'',
|
|
188
|
+
'## Purpose',
|
|
189
|
+
'',
|
|
190
|
+
'Human-facing control.',
|
|
191
|
+
'',
|
|
192
|
+
'## Scope',
|
|
193
|
+
'',
|
|
194
|
+
'Includes:',
|
|
195
|
+
'',
|
|
196
|
+
'- domain identity.',
|
|
197
|
+
'',
|
|
198
|
+
'## Invariants',
|
|
199
|
+
'',
|
|
200
|
+
'- Keep the domain stable.',
|
|
201
|
+
'',
|
|
202
|
+
'## Agent Document',
|
|
203
|
+
'',
|
|
204
|
+
'`other.md`',
|
|
205
|
+
'',
|
|
206
|
+
].join('\n'),
|
|
207
|
+
};
|
|
208
|
+
const fs = {
|
|
209
|
+
existsSync: (filePath) => [
|
|
210
|
+
'/project/ctx',
|
|
211
|
+
'/project/ctx/docs',
|
|
212
|
+
'/project/ctx/docs/product',
|
|
213
|
+
'/project/ctx/docs/architecture',
|
|
214
|
+
'/project/ctx/docs/environment',
|
|
215
|
+
'/project/ctx/docs/code',
|
|
216
|
+
'/project/ctx/adsm.json',
|
|
217
|
+
...Object.keys(files),
|
|
218
|
+
].includes(filePath),
|
|
219
|
+
readFileSync: (filePath) => filePath.endsWith('adsm.json')
|
|
220
|
+
? '{"schema":"adsm-context","version":"0.1.0"}'
|
|
221
|
+
: files[filePath],
|
|
222
|
+
readdirSync: (dirPath) => {
|
|
223
|
+
const tree = {
|
|
224
|
+
'/project/ctx/docs': dirents(['product', 'architecture', 'environment', 'code'], 'dir'),
|
|
225
|
+
'/project/ctx/docs/product': [
|
|
226
|
+
dirent('AGENTS.md', 'file'),
|
|
227
|
+
dirent('domain.md', 'file'),
|
|
228
|
+
dirent('domain.skin.en.md', 'file'),
|
|
229
|
+
],
|
|
230
|
+
'/project/ctx/docs/architecture': [],
|
|
231
|
+
'/project/ctx/docs/environment': [],
|
|
232
|
+
'/project/ctx/docs/code': [],
|
|
233
|
+
};
|
|
234
|
+
return tree[dirPath] ?? [];
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const service = new Service({
|
|
238
|
+
constants,
|
|
239
|
+
fs,
|
|
240
|
+
pathService: {join: (...parts) => parts.join('/'), resolve: (value) => value},
|
|
241
|
+
});
|
|
242
|
+
const result = await service.execute({targetPath: '/project'});
|
|
243
|
+
assert.equal(result.ok, false);
|
|
244
|
+
const findings = formatFindings(result.findings);
|
|
245
|
+
assert.match(findings, /Path mismatch: ctx\/docs\/product\/domain\.md declares ctx\/docs\/product\/wrong-domain\.md/);
|
|
246
|
+
assert.match(findings, /Missing Changed attribute: ctx\/docs\/product\/domain\.md/);
|
|
247
|
+
assert.match(findings, /Missing skin section "Mental Model": ctx\/docs\/product\/domain\.skin\.en\.md/);
|
|
248
|
+
assert.match(findings, /Missing Excludes: block in Scope: ctx\/docs\/product\/domain\.skin\.en\.md/);
|
|
249
|
+
assert.match(findings, /Skin agent document mismatch: ctx\/docs\/product\/domain\.skin\.en\.md references ctx\/docs\/product\/other\.md but expected ctx\/docs\/product\/domain\.md/);
|
|
250
|
+
assert.match(findings, /Missing paired agent document: ctx\/docs\/product\/domain\.skin\.en\.md references ctx\/docs\/product\/other\.md/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('Validate service reports broken local markdown links', async () => {
|
|
254
|
+
const files = {
|
|
255
|
+
'/project/ctx/docs/product/overview.md': doc({
|
|
256
|
+
path: 'ctx/docs/product/overview.md',
|
|
257
|
+
body: 'See [domain](./missing.md).\n',
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
const service = buildService({files});
|
|
261
|
+
const result = await service.execute({targetPath: '/project'});
|
|
262
|
+
assert.equal(result.ok, false);
|
|
263
|
+
assert.match(formatFindings(result.findings), /Missing linked document: \.\/missing\.md resolves to ctx\/docs\/product\/missing\.md/);
|
|
264
|
+
assert.equal(result.findings.some((entry) => entry.ruleId === 'local-link-exists' && entry.severity === 'error'), true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('Validate service accepts repository-local markdown links outside ctx/docs', async () => {
|
|
268
|
+
const files = {
|
|
269
|
+
'/project/README.md': '# Repository Root\n',
|
|
270
|
+
'/project/docs/overview.md': '# External Docs\n',
|
|
271
|
+
'/project/ctx/docs/product/overview.md': doc({
|
|
272
|
+
path: 'ctx/docs/product/overview.md',
|
|
273
|
+
body: 'Read [repo root](../../../README.md) and [external docs](../../../docs/overview.md).\n',
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
const service = buildService({files});
|
|
277
|
+
const result = await service.execute({targetPath: '/project'});
|
|
278
|
+
assert.equal(result.ok, true);
|
|
279
|
+
assert.equal(result.findings.some((entry) => entry.ruleId === 'local-link-exists' && entry.severity === 'error'), false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('Validate service reports warnings only for link fragments and skin warning thresholds', async () => {
|
|
283
|
+
const files = {
|
|
284
|
+
'/project/ctx/docs/product/domain.md': doc({
|
|
285
|
+
path: 'ctx/docs/product/domain.md',
|
|
286
|
+
body: 'Read [overview](./overview.md#purpose).\n',
|
|
287
|
+
}),
|
|
288
|
+
'/project/ctx/docs/product/domain.skin.en.md': skin({
|
|
289
|
+
extraLines: [
|
|
290
|
+
'- alpha.',
|
|
291
|
+
'- beta.',
|
|
292
|
+
'- gamma.',
|
|
293
|
+
'- delta.',
|
|
294
|
+
'- epsilon.',
|
|
295
|
+
'- zeta.',
|
|
296
|
+
'- eta.',
|
|
297
|
+
'- theta.',
|
|
298
|
+
'- iota.',
|
|
299
|
+
'- kappa.',
|
|
300
|
+
'- lambda.',
|
|
301
|
+
],
|
|
302
|
+
path: 'ctx/docs/product/domain.skin.en.md',
|
|
303
|
+
purpose: 'Keep human-facing purpose stable.',
|
|
304
|
+
agentDocument: 'domain.md',
|
|
305
|
+
}),
|
|
306
|
+
'/project/ctx/docs/product/overview.md': doc({
|
|
307
|
+
path: 'ctx/docs/product/overview.md',
|
|
308
|
+
body: 'Overview.\n',
|
|
309
|
+
}),
|
|
310
|
+
};
|
|
311
|
+
const service = buildService({files});
|
|
312
|
+
const result = await service.execute({targetPath: '/project'});
|
|
313
|
+
assert.equal(result.ok, true);
|
|
314
|
+
assert.equal(result.errorCount, 0);
|
|
315
|
+
assert.ok(result.warningCount >= 2);
|
|
316
|
+
const findings = formatFindings(result.findings);
|
|
317
|
+
assert.match(findings, /Link anchor was not validated/);
|
|
318
|
+
assert.match(findings, /Skin list count \d+ exceeds warning threshold 12/);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('Validate service measures only semantic skin content with Unicode-aware words', async () => {
|
|
322
|
+
const files = {
|
|
323
|
+
'/project/ctx/docs/product/domain.md': doc({
|
|
324
|
+
path: 'ctx/docs/product/domain.md',
|
|
325
|
+
body: 'Domain.\n',
|
|
326
|
+
}),
|
|
327
|
+
'/project/ctx/docs/product/domain.skin.en.md': [
|
|
328
|
+
'# Заголовок skin',
|
|
329
|
+
'',
|
|
330
|
+
'- Path: `ctx/docs/product/domain.skin.en.md`',
|
|
331
|
+
'- Changed: `20260715`',
|
|
332
|
+
'',
|
|
333
|
+
'## Purpose',
|
|
334
|
+
'',
|
|
335
|
+
'Привет мир',
|
|
336
|
+
'',
|
|
337
|
+
'## Mental Model',
|
|
338
|
+
'',
|
|
339
|
+
'## Scope',
|
|
340
|
+
'',
|
|
341
|
+
'Includes:',
|
|
342
|
+
'',
|
|
343
|
+
'- один',
|
|
344
|
+
'',
|
|
345
|
+
'Excludes:',
|
|
346
|
+
'',
|
|
347
|
+
'- два',
|
|
348
|
+
'',
|
|
349
|
+
'## Invariants',
|
|
350
|
+
'',
|
|
351
|
+
'## Agent Document',
|
|
352
|
+
'',
|
|
353
|
+
'`domain.md`',
|
|
354
|
+
'',
|
|
355
|
+
].join('\n'),
|
|
356
|
+
};
|
|
357
|
+
const service = buildService({
|
|
358
|
+
constantsOverride: {
|
|
359
|
+
SKIN_WORD_WARN: 3,
|
|
360
|
+
SKIN_WORD_ERROR: 5,
|
|
361
|
+
SKIN_LIST_WARN: 2,
|
|
362
|
+
SKIN_LIST_ERROR: 3,
|
|
363
|
+
},
|
|
364
|
+
files,
|
|
365
|
+
});
|
|
366
|
+
const result = await service.execute({targetPath: '/project'});
|
|
367
|
+
assert.equal(result.ok, true);
|
|
368
|
+
const findings = formatFindings(result.findings);
|
|
369
|
+
assert.match(findings, /Skin word count 4 exceeds warning threshold 3/);
|
|
370
|
+
assert.doesNotMatch(findings, /skin-size-list-count/);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('Validate service ignores skin headings and metadata when counting words', async () => {
|
|
374
|
+
const files = {
|
|
375
|
+
'/project/ctx/docs/product/domain.md': doc({
|
|
376
|
+
path: 'ctx/docs/product/domain.md',
|
|
377
|
+
body: 'Domain.\n',
|
|
378
|
+
}),
|
|
379
|
+
'/project/ctx/docs/product/domain.skin.en.md': [
|
|
380
|
+
'# Skin Title',
|
|
381
|
+
'',
|
|
382
|
+
'- Path: `ctx/docs/product/domain.skin.en.md`',
|
|
383
|
+
'- Changed: `20260708`',
|
|
384
|
+
'',
|
|
385
|
+
'## Purpose',
|
|
386
|
+
'',
|
|
387
|
+
'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen',
|
|
388
|
+
'',
|
|
389
|
+
'## Mental Model',
|
|
390
|
+
'',
|
|
391
|
+
'seventeen eighteen nineteen twenty',
|
|
392
|
+
'',
|
|
393
|
+
'## Scope',
|
|
394
|
+
'',
|
|
395
|
+
'Includes:',
|
|
396
|
+
'',
|
|
397
|
+
'- alpha.',
|
|
398
|
+
'',
|
|
399
|
+
'Excludes:',
|
|
400
|
+
'',
|
|
401
|
+
'- beta.',
|
|
402
|
+
'',
|
|
403
|
+
'## Invariants',
|
|
404
|
+
'',
|
|
405
|
+
'- gamma.',
|
|
406
|
+
'',
|
|
407
|
+
'## Agent Document',
|
|
408
|
+
'',
|
|
409
|
+
'`domain.md`',
|
|
410
|
+
'',
|
|
411
|
+
].join('\n'),
|
|
412
|
+
};
|
|
413
|
+
const service = buildService({
|
|
414
|
+
constantsOverride: {
|
|
415
|
+
SKIN_WORD_WARN: 30,
|
|
416
|
+
SKIN_WORD_ERROR: 40,
|
|
417
|
+
SKIN_LIST_WARN: 3,
|
|
418
|
+
SKIN_LIST_ERROR: 5,
|
|
419
|
+
},
|
|
420
|
+
files,
|
|
421
|
+
});
|
|
422
|
+
const result = await service.execute({targetPath: '/project'});
|
|
423
|
+
assert.equal(result.ok, true);
|
|
424
|
+
assert.equal(result.warningCount, 0);
|
|
425
|
+
assert.equal(result.findings.some((entry) => entry.ruleId === 'skin-size-word-count'), false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('Validate service reports errors for skin size limits', async () => {
|
|
429
|
+
const files = {
|
|
430
|
+
'/project/ctx/docs/product/domain.md': doc({
|
|
431
|
+
path: 'ctx/docs/product/domain.md',
|
|
432
|
+
body: 'Domain.\n',
|
|
433
|
+
}),
|
|
434
|
+
'/project/ctx/docs/product/domain.skin.en.md': skin({
|
|
435
|
+
extraLines: [
|
|
436
|
+
'- one.',
|
|
437
|
+
'- two.',
|
|
438
|
+
'- three.',
|
|
439
|
+
'- four.',
|
|
440
|
+
'- five.',
|
|
441
|
+
'- six.',
|
|
442
|
+
],
|
|
443
|
+
path: 'ctx/docs/product/domain.skin.en.md',
|
|
444
|
+
purpose: 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen',
|
|
445
|
+
agentDocument: 'domain.md',
|
|
446
|
+
}),
|
|
447
|
+
};
|
|
448
|
+
const service = buildService({
|
|
449
|
+
constantsOverride: {
|
|
450
|
+
SKIN_WORD_WARN: 12,
|
|
451
|
+
SKIN_WORD_ERROR: 20,
|
|
452
|
+
SKIN_LIST_WARN: 3,
|
|
453
|
+
SKIN_LIST_ERROR: 5,
|
|
454
|
+
},
|
|
455
|
+
files,
|
|
456
|
+
});
|
|
457
|
+
const result = await service.execute({targetPath: '/project'});
|
|
458
|
+
assert.equal(result.ok, false);
|
|
459
|
+
const findings = formatFindings(result.findings);
|
|
460
|
+
assert.match(findings, /Skin word count \d+ exceeds limit 20/);
|
|
461
|
+
assert.match(findings, /Skin list count \d+ exceeds limit 5/);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('Validate service applies the 160 and 240 word skin boundaries', async () => {
|
|
465
|
+
await assertSkinBoundary({
|
|
466
|
+
wordCount: 160,
|
|
467
|
+
expected: 'normal',
|
|
468
|
+
});
|
|
469
|
+
await assertSkinBoundary({
|
|
470
|
+
wordCount: 161,
|
|
471
|
+
expected: 'warning',
|
|
472
|
+
});
|
|
473
|
+
await assertSkinBoundary({
|
|
474
|
+
wordCount: 240,
|
|
475
|
+
expected: 'warning',
|
|
476
|
+
});
|
|
477
|
+
await assertSkinBoundary({
|
|
478
|
+
wordCount: 241,
|
|
479
|
+
expected: 'error',
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('Validate service warns about an intentional documentation level capacity exception', async () => {
|
|
484
|
+
const files = {
|
|
485
|
+
'/project/ctx/docs/product/AGENTS.md': doc({
|
|
486
|
+
path: 'ctx/docs/product/AGENTS.md',
|
|
487
|
+
body: '## Documentation Capacity Exception\n\nThis level intentionally has more than nine files because its stable concepts must remain directly discoverable.\n',
|
|
488
|
+
}),
|
|
489
|
+
...Object.fromEntries(Array.from({length: 10}, (_, index) => {
|
|
490
|
+
const name = `document-${index + 1}.md`;
|
|
491
|
+
return [`/project/ctx/docs/product/${name}`, doc({path: `ctx/docs/product/${name}`})];
|
|
492
|
+
})),
|
|
493
|
+
};
|
|
494
|
+
const service = buildService({files});
|
|
495
|
+
const result = await service.execute({targetPath: '/project'});
|
|
496
|
+
assert.equal(result.ok, true);
|
|
497
|
+
assert.match(formatFindings(result.findings), /warning doc-level-capacity ctx\/docs\/product Documentation level exceeds recommended capacity: 10 files/);
|
|
498
|
+
assert.equal(result.findings.some((entry) => entry.ruleId === 'doc-level-capacity-exception'), false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('Validate service requires an explicit capacity exception for overloaded documentation levels', async () => {
|
|
502
|
+
const files = {
|
|
503
|
+
'/project/ctx/docs/product/AGENTS.md': doc({
|
|
504
|
+
path: 'ctx/docs/product/AGENTS.md',
|
|
505
|
+
body: '## Local Rules\n\nKeep this level readable.\n',
|
|
506
|
+
}),
|
|
507
|
+
...Object.fromEntries(Array.from({length: 10}, (_, index) => {
|
|
508
|
+
const name = `document-${index + 1}.md`;
|
|
509
|
+
return [`/project/ctx/docs/product/${name}`, doc({path: `ctx/docs/product/${name}`})];
|
|
510
|
+
})),
|
|
511
|
+
};
|
|
512
|
+
const service = buildService({files});
|
|
513
|
+
const result = await service.execute({targetPath: '/project'});
|
|
514
|
+
assert.equal(result.ok, false);
|
|
515
|
+
assert.match(formatFindings(result.findings), /error doc-level-capacity-exception ctx\/docs\/product\/AGENTS\.md Documentation Capacity Exception must be explicit/);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('Validate service counts an agent document and its skin as one documentation file', async () => {
|
|
519
|
+
const files = {
|
|
520
|
+
'/project/ctx/docs/product/AGENTS.md': doc({
|
|
521
|
+
path: 'ctx/docs/product/AGENTS.md',
|
|
522
|
+
body: '## Local Rules\n\nKeep this level readable.\n',
|
|
523
|
+
}),
|
|
524
|
+
...Object.fromEntries(Array.from({length: 9}, (_, index) => {
|
|
525
|
+
const name = `document-${index + 1}`;
|
|
526
|
+
return [`/project/ctx/docs/product/${name}.md`, doc({path: `ctx/docs/product/${name}.md`})];
|
|
527
|
+
})),
|
|
528
|
+
...Object.fromEntries(Array.from({length: 9}, (_, index) => {
|
|
529
|
+
const name = `document-${index + 1}`;
|
|
530
|
+
return [`/project/ctx/docs/product/${name}.skin.en.md`, skin({
|
|
531
|
+
agentDocument: `${name}.md`,
|
|
532
|
+
path: `ctx/docs/product/${name}.skin.en.md`,
|
|
533
|
+
})];
|
|
534
|
+
})),
|
|
535
|
+
};
|
|
536
|
+
const service = buildService({files});
|
|
537
|
+
const result = await service.execute({targetPath: '/project'});
|
|
538
|
+
assert.equal(result.ok, true);
|
|
539
|
+
assert.equal(result.findings.some((entry) => entry.ruleId === 'doc-level-capacity'), false);
|
|
540
|
+
assert.equal(result.findings.some((entry) => entry.ruleId === 'doc-level-capacity-exception'), false);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test('Validate service checks the documentation level directory recommendation separately', async () => {
|
|
544
|
+
const agentsPath = '/project/ctx/docs/product/AGENTS.md';
|
|
545
|
+
const fs = {
|
|
546
|
+
existsSync: (filePath) => [
|
|
547
|
+
'/project/ctx',
|
|
548
|
+
'/project/ctx/docs',
|
|
549
|
+
'/project/ctx/docs/product',
|
|
550
|
+
'/project/ctx/docs/architecture',
|
|
551
|
+
'/project/ctx/docs/environment',
|
|
552
|
+
'/project/ctx/docs/code',
|
|
553
|
+
'/project/ctx/adsm.json',
|
|
554
|
+
agentsPath,
|
|
555
|
+
].includes(filePath),
|
|
556
|
+
readFileSync: (filePath) => filePath.endsWith('adsm.json')
|
|
557
|
+
? '{"schema":"adsm-context","version":"0.1.0"}'
|
|
558
|
+
: doc({
|
|
559
|
+
path: 'ctx/docs/product/AGENTS.md',
|
|
560
|
+
body: '## Documentation Capacity Exception\n\nTen direct directories intentionally separate independent product areas.\n',
|
|
561
|
+
}),
|
|
562
|
+
readdirSync: (dirPath) => {
|
|
563
|
+
if (dirPath === '/project/ctx/docs') return dirents(['product', 'architecture', 'environment', 'code'], 'dir');
|
|
564
|
+
if (dirPath === '/project/ctx/docs/product') {
|
|
565
|
+
return [dirent('AGENTS.md', 'file'), ...dirents(Array.from({length: 10}, (_, index) => `area-${index + 1}`), 'dir')];
|
|
566
|
+
}
|
|
567
|
+
return [];
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
const service = new Service({
|
|
571
|
+
constants,
|
|
572
|
+
fs,
|
|
573
|
+
pathService: {join: (...parts) => parts.join('/'), resolve: (value) => value},
|
|
574
|
+
});
|
|
575
|
+
const result = await service.execute({targetPath: '/project'});
|
|
576
|
+
assert.equal(result.ok, true);
|
|
577
|
+
assert.match(formatFindings(result.findings), /Documentation level exceeds recommended capacity: 10 directories/);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
function buildService({constantsOverride = {}, files}) {
|
|
581
|
+
const existingPaths = new Set([
|
|
582
|
+
'/project/ctx',
|
|
583
|
+
'/project/ctx/docs',
|
|
584
|
+
'/project/ctx/docs/product',
|
|
585
|
+
'/project/ctx/docs/architecture',
|
|
586
|
+
'/project/ctx/docs/environment',
|
|
587
|
+
'/project/ctx/docs/code',
|
|
588
|
+
'/project/ctx/adsm.json',
|
|
589
|
+
...Object.keys(files),
|
|
590
|
+
]);
|
|
591
|
+
const fs = {
|
|
592
|
+
existsSync: (filePath) => existingPaths.has(filePath),
|
|
593
|
+
readFileSync: (filePath) => filePath.endsWith('adsm.json')
|
|
594
|
+
? '{"schema":"adsm-context","version":"0.1.0"}'
|
|
595
|
+
: files[filePath],
|
|
596
|
+
readdirSync: (dirPath) => {
|
|
597
|
+
const tree = {
|
|
598
|
+
'/project/ctx/docs': dirents(['product', 'architecture', 'environment', 'code'], 'dir'),
|
|
599
|
+
'/project/ctx/docs/product': Object.keys(files)
|
|
600
|
+
.filter((entry) => entry.startsWith('/project/ctx/docs/product/'))
|
|
601
|
+
.map((entry) => entry.replace('/project/ctx/docs/product/', ''))
|
|
602
|
+
.filter((entry) => !entry.includes('/'))
|
|
603
|
+
.map((name) => dirent(name, 'file')),
|
|
604
|
+
'/project/ctx/docs/architecture': [],
|
|
605
|
+
'/project/ctx/docs/environment': [],
|
|
606
|
+
'/project/ctx/docs/code': [],
|
|
607
|
+
};
|
|
608
|
+
return tree[dirPath] ?? [];
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
return new Service({
|
|
613
|
+
constants: {...constants, ...constantsOverride},
|
|
614
|
+
fs,
|
|
615
|
+
pathService: {join: (...parts) => parts.join('/'), resolve: (value) => value},
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function assertSkinBoundary({wordCount, expected}) {
|
|
620
|
+
const skinText = skinWithWordCount({
|
|
621
|
+
agentDocument: 'domain.md',
|
|
622
|
+
path: 'ctx/docs/product/domain.skin.en.md',
|
|
623
|
+
wordCount,
|
|
624
|
+
});
|
|
625
|
+
const files = {
|
|
626
|
+
'/project/ctx/docs/product/domain.md': doc({
|
|
627
|
+
path: 'ctx/docs/product/domain.md',
|
|
628
|
+
body: 'Domain.\n',
|
|
629
|
+
}),
|
|
630
|
+
'/project/ctx/docs/product/domain.skin.en.md': skinText,
|
|
631
|
+
};
|
|
632
|
+
const service = buildService({
|
|
633
|
+
constantsOverride: {SKIN_LIST_WARN: 100, SKIN_LIST_ERROR: 101},
|
|
634
|
+
files,
|
|
635
|
+
});
|
|
636
|
+
const result = await service.execute({targetPath: '/project'});
|
|
637
|
+
const findings = formatFindings(result.findings);
|
|
638
|
+
if (expected === 'normal') {
|
|
639
|
+
assert.equal(result.warningCount, 0);
|
|
640
|
+
assert.equal(result.errorCount, 0);
|
|
641
|
+
assert.equal(result.ok, true);
|
|
642
|
+
assert.equal(findings.includes('Skin word count'), false);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (expected === 'warning') {
|
|
646
|
+
assert.equal(result.errorCount, 0);
|
|
647
|
+
assert.equal(result.warningCount, 1);
|
|
648
|
+
assert.match(findings, new RegExp(`Skin word count ${wordCount} exceeds warning threshold 160`));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
assert.equal(result.ok, false);
|
|
652
|
+
assert.equal(result.errorCount, 1);
|
|
653
|
+
assert.match(findings, new RegExp(`Skin word count ${wordCount} exceeds limit 240`));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatFindings(findings) {
|
|
657
|
+
return findings.map((entry) => `${entry.severity} ${entry.ruleId} ${entry.path} ${entry.message}`).join('\n');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function dirents(names, type) {
|
|
661
|
+
return names.map((name) => dirent(name, type));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function dirent(name, type) {
|
|
665
|
+
return {
|
|
666
|
+
name,
|
|
667
|
+
isDirectory: () => type === 'dir',
|
|
668
|
+
isFile: () => type === 'file',
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function doc({path, body = ''}) {
|
|
673
|
+
return [
|
|
674
|
+
'# Document',
|
|
675
|
+
'',
|
|
676
|
+
`- Path: \`${path}\``,
|
|
677
|
+
'- Changed: `20260708`',
|
|
678
|
+
'',
|
|
679
|
+
body.trimEnd(),
|
|
680
|
+
'',
|
|
681
|
+
].join('\n');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function skin({agentDocument, extraLines = [], path, purpose = 'Keep human-facing purpose stable.'}) {
|
|
685
|
+
return [
|
|
686
|
+
'# Skin',
|
|
687
|
+
'',
|
|
688
|
+
`- Path: \`${path}\``,
|
|
689
|
+
'- Changed: `20260708`',
|
|
690
|
+
'',
|
|
691
|
+
'## Purpose',
|
|
692
|
+
'',
|
|
693
|
+
purpose,
|
|
694
|
+
'',
|
|
695
|
+
'## Mental Model',
|
|
696
|
+
'',
|
|
697
|
+
'This is the human semantic frame.',
|
|
698
|
+
'',
|
|
699
|
+
'## Scope',
|
|
700
|
+
'',
|
|
701
|
+
'Includes:',
|
|
702
|
+
'',
|
|
703
|
+
'- the documented object.',
|
|
704
|
+
'',
|
|
705
|
+
'Excludes:',
|
|
706
|
+
'',
|
|
707
|
+
'- operational overload.',
|
|
708
|
+
'',
|
|
709
|
+
'## Invariants',
|
|
710
|
+
'',
|
|
711
|
+
'- Keep the documented object stable.',
|
|
712
|
+
...extraLines,
|
|
713
|
+
'',
|
|
714
|
+
'## Agent Document',
|
|
715
|
+
'',
|
|
716
|
+
`\`${agentDocument}\``,
|
|
717
|
+
'',
|
|
718
|
+
].join('\n');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function skinWithWordCount({agentDocument, path, wordCount}) {
|
|
722
|
+
const base = skin({
|
|
723
|
+
agentDocument,
|
|
724
|
+
path,
|
|
725
|
+
purpose: '',
|
|
726
|
+
});
|
|
727
|
+
const overhead = countWords(extractSemanticSkinText(base));
|
|
728
|
+
const extraWords = Math.max(0, wordCount - overhead);
|
|
729
|
+
const purpose = Array.from({length: extraWords}, () => 'alpha').join(' ');
|
|
730
|
+
return skin({
|
|
731
|
+
agentDocument,
|
|
732
|
+
path,
|
|
733
|
+
purpose,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function countWords(text) {
|
|
738
|
+
const matches = text.match(/[A-Za-z0-9][A-Za-z0-9'-]*/g);
|
|
739
|
+
return matches ? matches.length : 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function extractSemanticSkinText(text) {
|
|
743
|
+
return ['Purpose', 'Mental Model', 'Scope', 'Invariants']
|
|
744
|
+
.map((sectionName) => {
|
|
745
|
+
const body = [];
|
|
746
|
+
let insideSection = false;
|
|
747
|
+
for (const rawLine of text.split('\n')) {
|
|
748
|
+
const line = rawLine.trim();
|
|
749
|
+
if (!insideSection) {
|
|
750
|
+
if (line === `## ${sectionName}`) insideSection = true;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (line.startsWith('## ')) break;
|
|
754
|
+
body.push(rawLine);
|
|
755
|
+
}
|
|
756
|
+
return body.join('\n');
|
|
757
|
+
})
|
|
758
|
+
.join('\n')
|
|
759
|
+
.replace(/^\s*(?:Includes|Excludes):\s*$/gm, '');
|
|
760
|
+
}
|