artifact-chain-assistant 0.8.2 → 0.8.3

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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/INSTALL.md +10 -10
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +4 -4
  7. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  8. package/adapters/claude/INSTALL.md +10 -10
  9. package/adapters/claude/agent-methods/catalog.yaml +1 -1
  10. package/adapters/claude/compatibility.json +4 -4
  11. package/adapters/claude/families-src/prd-feature/implementation.yaml +39 -0
  12. package/adapters/claude/families-src/scenario-script/implementation.yaml +39 -0
  13. package/adapters/claude/family-apis/catalog.json +16 -0
  14. package/adapters/claude/family-apis/e2e-test/api.json +205 -0
  15. package/adapters/claude/family-apis/schema/family-api.schema.json +235 -0
  16. package/adapters/claude/schemas/project-facts.schema.json +80 -0
  17. package/adapters/claude/scripts/check-family-api.mjs +84 -0
  18. package/adapters/claude/scripts/export-family-api.mjs +184 -0
  19. package/adapters/claude/scripts/family-compile.mjs +309 -0
  20. package/adapters/claude/scripts/family-help-render.mjs +214 -0
  21. package/adapters/claude/scripts/lib/family-api-validator.mjs +413 -0
  22. package/adapters/claude/scripts/lib/generated/canonical-json-v1-manifest.json +7 -0
  23. package/adapters/claude/scripts/lib/generated/canonical-json-v1-vectors.generated.json +50 -0
  24. package/adapters/claude/scripts/lib/generated/canonical-json-v1.generated.mjs +130 -0
  25. package/adapters/claude/scripts/lib/generated/family-api-validator.generated.mjs +1756 -0
  26. package/adapters/claude/scripts/lib/generated/project-facts-validator.generated.mjs +638 -0
  27. package/adapters/claude/scripts/lib/generated/schema-validator-manifest.json +20 -0
  28. package/adapters/claude/scripts/lib/method-query.mjs +177 -0
  29. package/adapters/claude/scripts/method-query.mjs +67 -0
  30. package/adapters/claude/skills/setup/SKILL.md +2 -1
  31. package/adapters/codex/.codex-plugin/plugin.json +1 -1
  32. package/adapters/codex/INSTALL.md +10 -10
  33. package/adapters/codex/agent-methods/catalog.yaml +1 -1
  34. package/adapters/codex/compatibility.json +4 -4
  35. package/adapters/codex/families-src/prd-feature/implementation.yaml +39 -0
  36. package/adapters/codex/families-src/scenario-script/implementation.yaml +39 -0
  37. package/adapters/codex/family-apis/catalog.json +16 -0
  38. package/adapters/codex/family-apis/e2e-test/api.json +205 -0
  39. package/adapters/codex/family-apis/schema/family-api.schema.json +235 -0
  40. package/adapters/codex/schemas/project-facts.schema.json +80 -0
  41. package/adapters/codex/scripts/check-family-api.mjs +84 -0
  42. package/adapters/codex/scripts/export-family-api.mjs +184 -0
  43. package/adapters/codex/scripts/family-compile.mjs +309 -0
  44. package/adapters/codex/scripts/family-help-render.mjs +214 -0
  45. package/adapters/codex/scripts/lib/family-api-validator.mjs +413 -0
  46. package/adapters/codex/scripts/lib/generated/canonical-json-v1-manifest.json +7 -0
  47. package/adapters/codex/scripts/lib/generated/canonical-json-v1-vectors.generated.json +50 -0
  48. package/adapters/codex/scripts/lib/generated/canonical-json-v1.generated.mjs +130 -0
  49. package/adapters/codex/scripts/lib/generated/family-api-validator.generated.mjs +1756 -0
  50. package/adapters/codex/scripts/lib/generated/project-facts-validator.generated.mjs +638 -0
  51. package/adapters/codex/scripts/lib/generated/schema-validator-manifest.json +20 -0
  52. package/adapters/codex/scripts/lib/method-query.mjs +177 -0
  53. package/adapters/codex/scripts/method-query.mjs +67 -0
  54. package/adapters/codex/skills/setup/SKILL.md +2 -1
  55. package/agent-methods/catalog.yaml +1 -1
  56. package/compatibility.json +4 -4
  57. package/families-src/prd-feature/implementation.yaml +39 -0
  58. package/families-src/scenario-script/implementation.yaml +39 -0
  59. package/family-apis/catalog.json +16 -0
  60. package/family-apis/e2e-test/api.json +205 -0
  61. package/family-apis/schema/family-api.schema.json +235 -0
  62. package/package.json +2 -2
  63. package/schemas/project-facts.schema.json +80 -0
  64. package/scripts/check-family-api.mjs +84 -0
  65. package/scripts/export-family-api.mjs +184 -0
  66. package/scripts/family-compile.mjs +309 -0
  67. package/scripts/family-help-render.mjs +214 -0
  68. package/scripts/generate-canonical-json-protocol.mjs +75 -0
  69. package/scripts/generate-schema-validators.mjs +141 -0
  70. package/scripts/lib/family-api-validator.mjs +413 -0
  71. package/scripts/lib/generated/canonical-json-v1-manifest.json +7 -0
  72. package/scripts/lib/generated/canonical-json-v1-vectors.generated.json +50 -0
  73. package/scripts/lib/generated/canonical-json-v1.generated.mjs +130 -0
  74. package/scripts/lib/generated/family-api-validator.generated.mjs +1756 -0
  75. package/scripts/lib/generated/project-facts-validator.generated.mjs +638 -0
  76. package/scripts/lib/generated/schema-validator-manifest.json +20 -0
  77. package/scripts/lib/method-query.mjs +177 -0
  78. package/scripts/method-query.mjs +67 -0
  79. package/skills/setup/SKILL.md +2 -1
  80. package/skills-src/setup/SKILL.md +2 -1
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+ // @feature ACA18 @scenario S-65 @decision D-ACA-18
3
+ // Thin full-family compiler facade. API-only validation belongs to
4
+ // check-family-api.mjs. Full conformance integrates the authoritative
5
+ // Registry v2 verifier for independent implementation verification.
6
+ import { execFileSync } from 'node:child_process';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { loadJson, validateCatalogConsistency, validateFamilyApi } from './lib/family-api-validator.mjs';
11
+ import { parseMinimalYaml } from './lib/inline-yaml-parser.mjs';
12
+
13
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
14
+
15
+ export function subprocessCommandRunner({ command, args, cwd, timeout }) {
16
+ return {
17
+ stdout: execFileSync(command, args, { encoding: 'utf8', cwd, timeout }),
18
+ };
19
+ }
20
+
21
+ export async function familyCompile({
22
+ familyApiPath,
23
+ catalogPath,
24
+ implementationPath,
25
+ pluginRoot,
26
+ artifactGraphCommand,
27
+ registryCommand,
28
+ commandRunner = subprocessCommandRunner,
29
+ }) {
30
+ const base = pluginRoot || root;
31
+ const checks = [];
32
+ const result = {
33
+ ok: false,
34
+ apiConformance: { status: 'FAIL' },
35
+ familyConformance: { status: 'FAIL' },
36
+ deterministicConformance: { status: 'FAIL', checks, attestations: [] },
37
+ behaviorQualification: {
38
+ status: 'NOT_RUN',
39
+ evidence: [],
40
+ limitations: [
41
+ 'Family behavior TCK has not run',
42
+ 'Behavior qualification cannot prove general LLM semantic quality',
43
+ ],
44
+ },
45
+ };
46
+
47
+ let api;
48
+ const apiCheck = { name: 'family-api-validation', ok: false, status: 'FAIL', errors: [] };
49
+ try {
50
+ api = await loadJson(familyApiPath);
51
+ const validation = await validateFamilyApi(api, { sourcePath: familyApiPath });
52
+ apiCheck.ok = validation.ok;
53
+ apiCheck.status = validation.ok ? 'PASS' : 'FAIL';
54
+ apiCheck.errors = validation.errors.map(error => typeof error === 'string' ? error : `${error.code} ${error.path}: ${error.message}`);
55
+ apiCheck.warnings = validation.warnings;
56
+ } catch (error) {
57
+ apiCheck.errors.push(`Failed to load API file: ${error.message}`);
58
+ }
59
+ checks.push(apiCheck);
60
+
61
+ const catalogCheck = { name: 'catalog-consistency', ok: false, status: 'FAIL', errors: [] };
62
+ try {
63
+ const catalog = await loadJson(catalogPath || join(base, 'family-apis/catalog.json'));
64
+ if (!api) api = await loadJson(familyApiPath);
65
+ const validation = validateCatalogConsistency(catalog, [{
66
+ id: api.api?.id,
67
+ major: api.api?.major,
68
+ content: api,
69
+ }]);
70
+ catalogCheck.ok = validation.ok;
71
+ catalogCheck.status = validation.ok ? 'PASS' : 'FAIL';
72
+ catalogCheck.errors = validation.errors;
73
+ } catch (error) {
74
+ catalogCheck.errors.push(`Catalog consistency check failed: ${error.message}`);
75
+ }
76
+ checks.push(catalogCheck);
77
+
78
+ result.apiConformance.status = apiCheck.ok && catalogCheck.ok ? 'PASS' : 'FAIL';
79
+
80
+ const contractCheck = {
81
+ name: 'artifact-contract-validation',
82
+ ok: false,
83
+ status: 'FAIL',
84
+ errors: [],
85
+ resolvedContracts: [],
86
+ };
87
+ const command = artifactGraphCommand || findArtifactGraphCli(base);
88
+ const contractRefs = collectContractRefs(api);
89
+ if (!command) {
90
+ contractCheck.errors.push('artifact-graph CLI not found; authoritative contract validation is required');
91
+ } else {
92
+ let allResolved = true;
93
+ for (const contractRef of contractRefs) {
94
+ try {
95
+ const response = await commandRunner({
96
+ command,
97
+ args: ['contract', 'explain', '--contract', contractRef, '--format', 'json'],
98
+ cwd: base,
99
+ timeout: 30000,
100
+ });
101
+ const parsed = JSON.parse(response.stdout);
102
+ const revisionDigest = parsed?.data?.identity?.revisionDigest;
103
+ if (parsed?.ok !== true || !/^sha256:[a-f0-9]{64}$/.test(revisionDigest || '')) {
104
+ throw new Error('contract explain returned no authoritative revision digest');
105
+ }
106
+ contractCheck.resolvedContracts.push({ ref: contractRef, revisionDigest, status: 'RESOLVED' });
107
+ } catch (error) {
108
+ allResolved = false;
109
+ contractCheck.errors.push(`contract explain for ${contractRef} failed: ${error.message}`);
110
+ contractCheck.resolvedContracts.push({ ref: contractRef, status: 'FAIL' });
111
+ }
112
+ }
113
+ contractCheck.ok = allResolved;
114
+ contractCheck.status = allResolved ? 'PASS' : 'FAIL';
115
+ }
116
+ checks.push(contractCheck);
117
+
118
+ const implementationCheck = {
119
+ name: 'implementation-descriptor-presence',
120
+ ok: false,
121
+ status: 'IMPLEMENTATION_REQUIRED',
122
+ errors: [],
123
+ };
124
+ const registryCheck = {
125
+ name: 'registry-v2-validation',
126
+ ok: false,
127
+ status: 'REGISTRY_V2_REQUIRED',
128
+ errors: [],
129
+ };
130
+
131
+ let implementationDescriptor = null;
132
+
133
+ if (!implementationPath) {
134
+ implementationCheck.errors.push('Full family compile requires --implementation=<implementation.yaml>');
135
+ } else if (!existsSync(implementationPath)) {
136
+ implementationCheck.errors.push(`Implementation descriptor not found: ${implementationPath}`);
137
+ } else {
138
+ // Step 1: Invoke Registry v2 validate command
139
+ const registryCmd = registryCommand || findRegistryCli(base);
140
+ if (!registryCmd) {
141
+ registryCheck.errors.push('Registry v2 CLI not found; authoritative validation is required');
142
+ registryCheck.status = 'REGISTRY_V2_REQUIRED';
143
+ } else {
144
+ try {
145
+ const registryResponse = await commandRunner({
146
+ command: registryCmd,
147
+ args: ['validate', '--catalog', implementationPath],
148
+ cwd: base,
149
+ timeout: 30000,
150
+ });
151
+ const registryParsed = JSON.parse(registryResponse.stdout);
152
+ if (registryParsed?.ok !== true) {
153
+ const diagnostics = (registryParsed?.diagnostics || [])
154
+ .filter(d => d.severity === 'error')
155
+ .map(d => d.message || d.code)
156
+ .join('; ');
157
+ throw new Error(`Registry v2 validation failed: ${diagnostics || 'ok=false'}`);
158
+ }
159
+ registryCheck.ok = true;
160
+ registryCheck.status = 'PASS';
161
+
162
+ // Step 2: Read and parse implementation descriptor using fail-closed YAML parser
163
+ const yamlSource = readFileSync(implementationPath, 'utf8');
164
+ implementationDescriptor = parseMinimalYaml(yamlSource);
165
+ if (!implementationDescriptor || typeof implementationDescriptor !== 'object') {
166
+ throw new Error('Failed to parse implementation descriptor as valid YAML mapping');
167
+ }
168
+
169
+ // Step 3: Execute assistant-owned API/implementation conformance checks
170
+ const conformanceErrors = validateImplementationConformance(api, implementationDescriptor);
171
+ if (conformanceErrors.length > 0) {
172
+ implementationCheck.errors.push(...conformanceErrors);
173
+ implementationCheck.status = 'CONFORMANCE_FAIL';
174
+ } else {
175
+ implementationCheck.ok = true;
176
+ implementationCheck.status = 'PASS';
177
+ }
178
+ } catch (error) {
179
+ registryCheck.errors.push(`Registry v2 validation failed: ${error.message}`);
180
+ registryCheck.status = 'FAIL';
181
+ implementationCheck.errors.push(`Implementation verification failed: ${error.message}`);
182
+ }
183
+ }
184
+ }
185
+ checks.push(implementationCheck);
186
+ checks.push(registryCheck);
187
+
188
+ const allPass = checks.every(check => check.ok === true);
189
+ result.deterministicConformance.status = allPass ? 'PASS' : 'FAIL';
190
+ result.familyConformance.status = allPass ? 'PASS' : 'FAIL';
191
+ result.ok = allPass;
192
+ return result;
193
+ }
194
+
195
+ function collectContractRefs(api) {
196
+ const refs = new Set();
197
+ for (const service of api?.services || []) {
198
+ for (const ref of [...(service.accepts || []), ...(service.produces || [])]) {
199
+ if (typeof ref === 'string' && /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*@[0-9]+$/.test(ref)) refs.add(ref);
200
+ }
201
+ }
202
+ return [...refs].sort();
203
+ }
204
+
205
+ function findArtifactGraphCli(base) {
206
+ const candidates = [
207
+ join(base, 'node_modules/.bin/artifact-graph'),
208
+ join(base, '../../node_modules/.bin/artifact-graph'),
209
+ ];
210
+ return candidates.find(candidate => existsSync(candidate)) || null;
211
+ }
212
+
213
+ function findRegistryCli(base) {
214
+ // Deterministic search in plugin and parent project node_modules
215
+ const candidates = [
216
+ join(base, 'node_modules/.bin/agent-method-registry'),
217
+ join(base, '../../node_modules/.bin/agent-method-registry'),
218
+ ];
219
+ return candidates.find(candidate => existsSync(candidate)) || null;
220
+ }
221
+
222
+ /**
223
+ * Validate implementation conformance against Family API.
224
+ * Only checks what assistant owns: API identity match, required service coverage,
225
+ * and no unknown services. Structure validation is Registry's responsibility.
226
+ *
227
+ * @param {object} api - The loaded Family API
228
+ * @param {object} descriptor - The parsed implementation descriptor
229
+ * @returns {string[]} Array of conformance errors (empty if all pass)
230
+ */
231
+ function validateImplementationConformance(api, descriptor) {
232
+ const errors = [];
233
+
234
+ // Check implements block exists
235
+ const impl = descriptor.implements;
236
+ if (!impl || typeof impl !== 'object') {
237
+ errors.push('Implementation descriptor missing "implements" block');
238
+ return errors;
239
+ }
240
+
241
+ // Verify API identity match (exact match required)
242
+ const apiId = api?.api?.id;
243
+ const apiMajor = api?.api?.major;
244
+ const apiRevisionDigest = api?.api?.revisionDigest;
245
+
246
+ if (!apiId || apiMajor === undefined || !apiRevisionDigest) {
247
+ errors.push('Family API missing required identity fields (id, major, revisionDigest)');
248
+ return errors;
249
+ }
250
+
251
+ if (impl.apiId !== apiId) {
252
+ errors.push(`API id mismatch: expected "${apiId}", got "${impl.apiId}"`);
253
+ }
254
+ if (impl.apiMajor !== apiMajor) {
255
+ errors.push(`API major version mismatch: expected ${apiMajor}, got ${impl.apiMajor}`);
256
+ }
257
+ if (impl.apiRevisionDigest !== apiRevisionDigest) {
258
+ errors.push(`API revision digest mismatch: expected "${apiRevisionDigest}", got "${impl.apiRevisionDigest}"`);
259
+ }
260
+
261
+ // Check services conformance
262
+ const services = descriptor.services;
263
+ if (!services || typeof services !== 'object' || Array.isArray(services)) {
264
+ errors.push('"services" must be a mapping');
265
+ return errors;
266
+ }
267
+
268
+ const implementedServices = new Set(Object.keys(services));
269
+
270
+ // Check all required services are implemented
271
+ const requiredServices = (api?.services || [])
272
+ .filter(service => service.required === true)
273
+ .map(service => service.id);
274
+
275
+ for (const required of requiredServices) {
276
+ if (!implementedServices.has(required)) {
277
+ errors.push(`Required service not implemented: "${required}"`);
278
+ }
279
+ }
280
+
281
+ // Check no unknown services (not in API)
282
+ const apiServiceIds = new Set((api?.services || []).map(service => service.id));
283
+ for (const implemented of implementedServices) {
284
+ if (!apiServiceIds.has(implemented)) {
285
+ errors.push(`Unknown service not in API: "${implemented}"`);
286
+ }
287
+ }
288
+
289
+ return errors;
290
+ }
291
+
292
+ if (process.argv[1] && process.argv[1].endsWith('family-compile.mjs')) {
293
+ const apiArg = process.argv.find(argument => argument.startsWith('--api='));
294
+ if (!apiArg) {
295
+ process.stderr.write('Usage: family-compile.mjs --api=<api.json> --implementation=<implementation.yaml> [--catalog=<catalog.json>] [--registry-command=<path>]\n');
296
+ process.exit(1);
297
+ }
298
+ const catalogArg = process.argv.find(argument => argument.startsWith('--catalog='));
299
+ const implementationArg = process.argv.find(argument => argument.startsWith('--implementation='));
300
+ const registryCommandArg = process.argv.find(argument => argument.startsWith('--registry-command='));
301
+ const result = await familyCompile({
302
+ familyApiPath: apiArg.slice('--api='.length),
303
+ catalogPath: catalogArg?.slice('--catalog='.length),
304
+ implementationPath: implementationArg?.slice('--implementation='.length),
305
+ registryCommand: registryCommandArg?.slice('--registry-command='.length),
306
+ });
307
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
308
+ if (!result.ok) process.exit(1);
309
+ }
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ // @feature ACA18 @scenario S-65 @decision D-ACA-18
3
+ // Deterministic renderer for help skill.
4
+ // Reads Family API catalog, implementation samples, and optional projection;
5
+ // outputs Markdown help text. Fail-closed: projection parse failure → NOT_AVAILABLE.
6
+ import { readdir, readFile } from 'node:fs/promises';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { parseMinimalYaml } from './lib/inline-yaml-parser.mjs';
10
+ import { validateCatalogConsistency, validateFamilyApi } from './lib/family-api-validator.mjs';
11
+
12
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
13
+
14
+ export async function renderHelp({ projectionPath, pluginRoot } = {}) {
15
+ const base = pluginRoot || root;
16
+ const sections = [];
17
+
18
+ // 1. Load and render Family API Catalog
19
+ const catalogPath = join(base, 'family-apis', 'catalog.json');
20
+ let catalog;
21
+ try {
22
+ catalog = JSON.parse(await readFile(catalogPath, 'utf8'));
23
+ } catch {
24
+ sections.push('## Standard Family API\n\n> Family API catalog not found or invalid.\n');
25
+ return sections.join('\n---\n\n');
26
+ }
27
+
28
+ const apiSummary = await renderApiCatalog(catalog, base);
29
+ sections.push(apiSummary);
30
+
31
+ // 2. Load and render bundled legacy implementations
32
+ const familiesDir = join(base, 'families-src');
33
+ const legacySection = await renderLegacyMethods(familiesDir);
34
+ sections.push(legacySection);
35
+
36
+ // 3. Render installation status from optional projection
37
+ const statusSection = await renderInstallationStatus(projectionPath);
38
+ sections.push(statusSection);
39
+
40
+ // 4. Adoption steps
41
+ sections.push(renderAdoptionSteps());
42
+
43
+ return sections.join('\n---\n\n');
44
+ }
45
+
46
+ async function renderApiCatalog(catalog, base) {
47
+ const lines = ['## Standard Family API', ''];
48
+
49
+ if (!catalog.families || catalog.families.length === 0) {
50
+ lines.push('> No standard families registered.\n');
51
+ return lines.join('\n');
52
+ }
53
+
54
+ const loadedFamilies = [];
55
+ const diagnostics = [];
56
+ for (const family of catalog.families) {
57
+ const api = await loadApi(join(base, 'family-apis', family.apiPath));
58
+ if (!api) {
59
+ diagnostics.push(`${family.id}@${family.major}: API file missing or invalid JSON`);
60
+ continue;
61
+ }
62
+ const validation = await validateFamilyApi(api, { sourcePath: family.apiPath });
63
+ if (!validation.ok) {
64
+ diagnostics.push(...validation.errors.map(error => `${family.id}@${family.major}: ${error.code} ${error.path} ${error.message}`));
65
+ continue;
66
+ }
67
+ loadedFamilies.push({ family, api });
68
+ }
69
+
70
+ const consistency = validateCatalogConsistency(catalog, loadedFamilies.map(({ api }) => ({
71
+ id: api.api.id,
72
+ major: api.api.major,
73
+ content: api,
74
+ })));
75
+ diagnostics.push(...consistency.errors);
76
+ if (diagnostics.length > 0) {
77
+ lines.push('> Standard Family API catalog is unavailable because deterministic validation failed.');
78
+ lines.push('>');
79
+ for (const diagnostic of diagnostics) lines.push(`> - ${diagnostic}`);
80
+ return lines.join('\n');
81
+ }
82
+
83
+ // Summary table
84
+ lines.push('| Family | Major | Services | Summary |');
85
+ lines.push('|--------|-------|----------|---------|');
86
+
87
+ for (const { family, api } of loadedFamilies) {
88
+ const serviceNames = (api.services || []).map(s => s.id.split('.').pop()).join(', ');
89
+ lines.push(`| ${family.id} | ${family.major} | ${serviceNames} | ${family.summary || api.api?.summary || ''} |`);
90
+ }
91
+
92
+ // Detailed per-family sections
93
+ for (const { family, api } of loadedFamilies) {
94
+ lines.push('');
95
+ lines.push(`### ${family.id}@${family.major}`);
96
+ lines.push('');
97
+ lines.push(api.api?.summary || '');
98
+ lines.push('');
99
+ lines.push('**Services:**');
100
+
101
+ for (const svc of api.services || []) {
102
+ const required = svc.required ? 'required' : 'optional';
103
+ const discoverable = svc.discoverable?.default ? ' [DEFAULT]' : (svc.discoverable?.help ? ' [HELP]' : '');
104
+ lines.push(`- \`${svc.id.split('.').pop()}\` (${required}, ${svc.kind})${discoverable} — ${renderServiceSummary(svc)}`);
105
+ }
106
+
107
+ // Contract refs
108
+ const contractRefs = new Set();
109
+ for (const svc of api.services || []) {
110
+ for (const ref of [...(svc.accepts || []), ...(svc.produces || [])]) {
111
+ if (typeof ref === 'string' && ref.includes('@')) {
112
+ contractRefs.add(ref);
113
+ }
114
+ }
115
+ }
116
+ if (contractRefs.size > 0) {
117
+ lines.push(`**Artifact Contracts:** ${[...contractRefs].join(', ')}`);
118
+ }
119
+
120
+ // Capabilities
121
+ if (api.capabilities && api.capabilities.length > 0) {
122
+ lines.push('');
123
+ lines.push('**Capabilities:**');
124
+ for (const cap of api.capabilities) {
125
+ lines.push(`- ${cap.id}: ${cap.summary}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ return lines.join('\n');
131
+ }
132
+
133
+ function renderServiceSummary(svc) {
134
+ const parts = [];
135
+ if (svc.intents) parts.push(`intents: ${svc.intents.join(', ')}`);
136
+ if (svc.sideEffectCeiling) parts.push(`ceiling: ${svc.sideEffectCeiling}`);
137
+ return parts.length > 0 ? parts.join('; ') : svc.id;
138
+ }
139
+
140
+ async function renderLegacyMethods(familiesDir) {
141
+ const lines = ['## Bundled Legacy Methods', ''];
142
+ lines.push('These bundled families predate the standard Family API system.');
143
+ lines.push('They are recorded as migration samples and do NOT claim Family API conformance.');
144
+ lines.push('');
145
+
146
+ let found = false;
147
+ try {
148
+ for (const entry of await readdir(familiesDir, { withFileTypes: true })) {
149
+ if (!entry.isDirectory()) continue;
150
+ const implPath = join(familiesDir, entry.name, 'implementation.yaml');
151
+ try {
152
+ const content = await readFile(implPath, 'utf8');
153
+ const impl = parseMinimalYaml(content);
154
+ if (!impl) continue;
155
+ found = true;
156
+ const status = impl.targetApiStatus === 'pending' ? 'pending (no published Family API)' : impl.targetApiStatus || 'unknown';
157
+ const ownership = impl.lifecycle?.ownership || 'unknown';
158
+ const maturity = impl.lifecycle?.maturity || 'unknown';
159
+ lines.push(`### ${entry.name} (${maturity})`);
160
+ lines.push(`- Status: targetApiStatus: ${status}`);
161
+ lines.push(`- Ownership: ${ownership}`);
162
+ const serviceIds = impl.services ? Object.keys(impl.services) : [];
163
+ const serviceNames = serviceIds.map(s => s.split('.').pop());
164
+ lines.push(`- Services: ${serviceNames.join(', ')}`);
165
+ lines.push('- Note: Does NOT claim Family API conformance');
166
+ lines.push('');
167
+ } catch {
168
+ // No implementation.yaml — skip
169
+ }
170
+ }
171
+ } catch {
172
+ // families-src directory not found
173
+ }
174
+
175
+ if (!found) {
176
+ lines.push('> No bundled legacy family implementations found.\n');
177
+ }
178
+
179
+ return lines.join('\n');
180
+ }
181
+
182
+ async function renderInstallationStatus(projectionPath) {
183
+ const lines = ['## Installation Status', ''];
184
+ lines.push('Registry projection: **NOT_AVAILABLE** (REGISTRY_V2_REQUIRED)');
185
+ lines.push('');
186
+ lines.push('Dynamic provider state is intentionally unavailable until the Registry v2 projection verifier is integrated.');
187
+ if (projectionPath) lines.push('The supplied projection was ignored; assistant-side marker checks are not an authority.');
188
+ return lines.join('\n');
189
+ }
190
+
191
+ function renderAdoptionSteps() {
192
+ return `## Getting Started
193
+
194
+ 1. **Learn**: Review the Standard Family API above to understand available capabilities.
195
+ 2. **Bootstrap**: Run \`artifact-chain-bootstrap\` (with user authorization) to set up your project.
196
+ 3. **Orient**: Run \`where-am-i\` to get project-specific recommendations based on your current state.
197
+ 4. **Adopt**: Follow where-am-i recommendations to adopt specific families and services.`;
198
+ }
199
+
200
+ async function loadApi(path) {
201
+ try {
202
+ return JSON.parse(await readFile(path, 'utf8'));
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ // CLI entry
209
+ if (process.argv[1] && process.argv[1].endsWith('family-help-render.mjs')) {
210
+ const projectionArg = process.argv.find(a => a.startsWith('--projection='));
211
+ const projectionPath = projectionArg ? projectionArg.slice('--projection='.length) : undefined;
212
+ const md = await renderHelp({ projectionPath });
213
+ process.stdout.write(md + '\n');
214
+ }