artifact-chain-assistant 0.8.2 → 0.8.4
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -0
- package/INSTALL.md +10 -10
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/INSTALL.md +10 -10
- package/adapters/claude/agent-methods/catalog.yaml +1 -1
- package/adapters/claude/compatibility.json +4 -4
- package/adapters/claude/families-src/prd-feature/implementation.yaml +39 -0
- package/adapters/claude/families-src/scenario-script/implementation.yaml +39 -0
- package/adapters/claude/family-apis/catalog.json +16 -0
- package/adapters/claude/family-apis/e2e-test/api.json +205 -0
- package/adapters/claude/family-apis/schema/family-api.schema.json +235 -0
- package/adapters/claude/schemas/project-facts.schema.json +80 -0
- package/adapters/claude/scripts/check-family-api.mjs +84 -0
- package/adapters/claude/scripts/export-family-api.mjs +184 -0
- package/adapters/claude/scripts/family-compile.mjs +309 -0
- package/adapters/claude/scripts/family-help-render.mjs +214 -0
- package/adapters/claude/scripts/lib/family-api-validator.mjs +413 -0
- package/adapters/claude/scripts/lib/generated/canonical-json-v1-manifest.json +7 -0
- package/adapters/claude/scripts/lib/generated/canonical-json-v1-vectors.generated.json +50 -0
- package/adapters/claude/scripts/lib/generated/canonical-json-v1.generated.mjs +130 -0
- package/adapters/claude/scripts/lib/generated/family-api-validator.generated.mjs +1756 -0
- package/adapters/claude/scripts/lib/generated/project-facts-validator.generated.mjs +638 -0
- package/adapters/claude/scripts/lib/generated/schema-validator-manifest.json +20 -0
- package/adapters/claude/scripts/lib/method-query.mjs +177 -0
- package/adapters/claude/scripts/method-query.mjs +67 -0
- package/adapters/claude/skills/setup/SKILL.md +3 -2
- package/adapters/codex/.codex-plugin/plugin.json +1 -1
- package/adapters/codex/INSTALL.md +10 -10
- package/adapters/codex/agent-methods/catalog.yaml +1 -1
- package/adapters/codex/compatibility.json +4 -4
- package/adapters/codex/families-src/prd-feature/implementation.yaml +39 -0
- package/adapters/codex/families-src/scenario-script/implementation.yaml +39 -0
- package/adapters/codex/family-apis/catalog.json +16 -0
- package/adapters/codex/family-apis/e2e-test/api.json +205 -0
- package/adapters/codex/family-apis/schema/family-api.schema.json +235 -0
- package/adapters/codex/schemas/project-facts.schema.json +80 -0
- package/adapters/codex/scripts/check-family-api.mjs +84 -0
- package/adapters/codex/scripts/export-family-api.mjs +184 -0
- package/adapters/codex/scripts/family-compile.mjs +309 -0
- package/adapters/codex/scripts/family-help-render.mjs +214 -0
- package/adapters/codex/scripts/lib/family-api-validator.mjs +413 -0
- package/adapters/codex/scripts/lib/generated/canonical-json-v1-manifest.json +7 -0
- package/adapters/codex/scripts/lib/generated/canonical-json-v1-vectors.generated.json +50 -0
- package/adapters/codex/scripts/lib/generated/canonical-json-v1.generated.mjs +130 -0
- package/adapters/codex/scripts/lib/generated/family-api-validator.generated.mjs +1756 -0
- package/adapters/codex/scripts/lib/generated/project-facts-validator.generated.mjs +638 -0
- package/adapters/codex/scripts/lib/generated/schema-validator-manifest.json +20 -0
- package/adapters/codex/scripts/lib/method-query.mjs +177 -0
- package/adapters/codex/scripts/method-query.mjs +67 -0
- package/adapters/codex/skills/setup/SKILL.md +3 -2
- package/agent-methods/catalog.yaml +1 -1
- package/compatibility.json +4 -4
- package/families-src/prd-feature/implementation.yaml +39 -0
- package/families-src/scenario-script/implementation.yaml +39 -0
- package/family-apis/catalog.json +16 -0
- package/family-apis/e2e-test/api.json +205 -0
- package/family-apis/schema/family-api.schema.json +235 -0
- package/package.json +2 -2
- package/schemas/project-facts.schema.json +80 -0
- package/scripts/check-family-api.mjs +84 -0
- package/scripts/export-family-api.mjs +184 -0
- package/scripts/family-compile.mjs +309 -0
- package/scripts/family-help-render.mjs +214 -0
- package/scripts/generate-canonical-json-protocol.mjs +75 -0
- package/scripts/generate-schema-validators.mjs +141 -0
- package/scripts/lib/family-api-validator.mjs +413 -0
- package/scripts/lib/generated/canonical-json-v1-manifest.json +7 -0
- package/scripts/lib/generated/canonical-json-v1-vectors.generated.json +50 -0
- package/scripts/lib/generated/canonical-json-v1.generated.mjs +130 -0
- package/scripts/lib/generated/family-api-validator.generated.mjs +1756 -0
- package/scripts/lib/generated/project-facts-validator.generated.mjs +638 -0
- package/scripts/lib/generated/schema-validator-manifest.json +20 -0
- package/scripts/lib/method-query.mjs +177 -0
- package/scripts/method-query.mjs +67 -0
- package/skills/setup/SKILL.md +3 -2
- package/skills-src/setup/SKILL.md +3 -2
- package/templates/git-hooks/pre-commit.sh +5 -2
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://artifact-chain.dev/schemas/family-api.schema.json",
|
|
4
|
+
"title": "Skill Family API",
|
|
5
|
+
"description": "Schema for standard Skill Family API definitions owned by artifact-chain-assistant",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "api", "services"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"type": "integer",
|
|
12
|
+
"const": 1
|
|
13
|
+
},
|
|
14
|
+
"api": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["id", "major", "revisionDigest", "displayName", "summary"],
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^artifact\\.[a-z][a-z0-9-]*-family$",
|
|
22
|
+
"description": "Canonical family API identity in the artifact.* namespace"
|
|
23
|
+
},
|
|
24
|
+
"major": {
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"minimum": 1,
|
|
27
|
+
"description": "Major version indicating compatibility family"
|
|
28
|
+
},
|
|
29
|
+
"revisionDigest": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^sha256:[a-f0-9]{64}$",
|
|
32
|
+
"description": "Immutable SHA-256 digest of canonical API content excluding this field"
|
|
33
|
+
},
|
|
34
|
+
"displayName": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"minLength": 1
|
|
37
|
+
},
|
|
38
|
+
"summary": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"minLength": 1
|
|
41
|
+
},
|
|
42
|
+
"artifactTypes": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": { "type": "string", "minLength": 1 },
|
|
45
|
+
"uniqueItems": true,
|
|
46
|
+
"description": "Artifact types this family operates on"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"services": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"minItems": 1,
|
|
53
|
+
"items": {
|
|
54
|
+
"$ref": "#/definitions/service"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"capabilities": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"$ref": "#/definitions/capability"
|
|
61
|
+
},
|
|
62
|
+
"description": "Optional public capabilities (no implementation state)"
|
|
63
|
+
},
|
|
64
|
+
"dataTransferObjects": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "$ref": "#/definitions/dataTransferObject" },
|
|
67
|
+
"description": "Family API 拥有的结构化中间或正式 DTO 声明;schema 仍由 family 提供,接口编译器只校验声明形状和服务引用。"
|
|
68
|
+
},
|
|
69
|
+
"qualificationKinds": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": { "type": "string", "pattern": "^[a-z][A-Za-z0-9]+$" },
|
|
72
|
+
"uniqueItems": true,
|
|
73
|
+
"description": "该 Family API 区分的资格种类名称;只声明种类,不把资格证据或成熟度写入接口。"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"definitions": {
|
|
77
|
+
"service": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["id", "required", "kind", "intents"],
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"properties": {
|
|
82
|
+
"id": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"pattern": "^artifact\\.[a-z][a-z0-9-]+\\.[a-z][a-z0-9-]+$",
|
|
85
|
+
"description": "Canonical service identity: artifact.<family>.<intent>"
|
|
86
|
+
},
|
|
87
|
+
"required": {
|
|
88
|
+
"type": "boolean",
|
|
89
|
+
"description": "Whether this service is required for API conformance"
|
|
90
|
+
},
|
|
91
|
+
"kind": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["workflow", "operation"],
|
|
94
|
+
"description": "workflow = stateful multi-step; operation = stateless single-step"
|
|
95
|
+
},
|
|
96
|
+
"intents": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"minItems": 1,
|
|
99
|
+
"items": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"enum": ["default", "help", "author", "review", "repair", "route", "audit", "generate", "batch"]
|
|
102
|
+
},
|
|
103
|
+
"uniqueItems": true
|
|
104
|
+
},
|
|
105
|
+
"accepts": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"anyOf": [
|
|
109
|
+
{ "type": "string", "minLength": 1 },
|
|
110
|
+
{ "$ref": "#/definitions/protocolRef" }
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"description": "Input contract references or natural language descriptions"
|
|
114
|
+
},
|
|
115
|
+
"produces": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"items": {
|
|
118
|
+
"anyOf": [
|
|
119
|
+
{ "type": "string", "minLength": 1 },
|
|
120
|
+
{ "$ref": "#/definitions/protocolRef" }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"description": "Output contract references or natural language descriptions"
|
|
124
|
+
},
|
|
125
|
+
"sideEffectCeiling": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"enum": ["none", "read-only", "write-authorized-artifacts", "write-review-result", "write-project-artifacts"],
|
|
128
|
+
"description": "Maximum side effect level this service may produce"
|
|
129
|
+
},
|
|
130
|
+
"mixSafe": {
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"default": false,
|
|
133
|
+
"description": "Whether this service can be bound to a different family implementation than other services in the same family"
|
|
134
|
+
},
|
|
135
|
+
"discoverable": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"properties": {
|
|
139
|
+
"default": {
|
|
140
|
+
"type": "boolean",
|
|
141
|
+
"description": "Whether this is the default entry point"
|
|
142
|
+
},
|
|
143
|
+
"help": {
|
|
144
|
+
"type": "boolean",
|
|
145
|
+
"description": "Whether this is the help entry point"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"modes": {
|
|
150
|
+
"$ref": "#/definitions/serviceModes"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"capability": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"required": ["id", "summary"],
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"properties": {
|
|
159
|
+
"id": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"minLength": 1
|
|
162
|
+
},
|
|
163
|
+
"summary": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"minLength": 1
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"serviceModes": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"required": ["supported", "default"],
|
|
172
|
+
"additionalProperties": false,
|
|
173
|
+
"properties": {
|
|
174
|
+
"supported": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"minItems": 1,
|
|
177
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
178
|
+
"uniqueItems": true
|
|
179
|
+
},
|
|
180
|
+
"default": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"dataTransferObject": {
|
|
184
|
+
"type": "object",
|
|
185
|
+
"required": ["id", "schemaId", "schema", "version", "digestRule", "services"],
|
|
186
|
+
"additionalProperties": false,
|
|
187
|
+
"properties": {
|
|
188
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
189
|
+
"schemaId": { "type": "string", "minLength": 1 },
|
|
190
|
+
"schema": { "type": "string", "pattern": "^[^/\\\\~].*\\.json$" },
|
|
191
|
+
"version": { "type": "string", "minLength": 1 },
|
|
192
|
+
"digestRule": { "type": "string", "minLength": 1 },
|
|
193
|
+
"artifactBinding": { "type": "string", "minLength": 1 },
|
|
194
|
+
"services": {
|
|
195
|
+
"type": "array",
|
|
196
|
+
"items": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"required": ["service", "position"],
|
|
199
|
+
"additionalProperties": false,
|
|
200
|
+
"properties": {
|
|
201
|
+
"service": { "type": "string", "pattern": "^artifact\\.[a-z][a-z0-9-]+\\.[a-z][a-z0-9-]+$" },
|
|
202
|
+
"position": { "type": "string", "minLength": 1 }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"contractRef": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*@[0-9]+$",
|
|
211
|
+
"description": "Artifact Contract major identity reference (e.g., artifact.e2e-test@1)"
|
|
212
|
+
},
|
|
213
|
+
"protocolRef": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"required": ["protocol", "version"],
|
|
216
|
+
"additionalProperties": false,
|
|
217
|
+
"properties": {
|
|
218
|
+
"protocol": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"minLength": 1,
|
|
221
|
+
"description": "Protocol name when no formal Artifact Contract exists"
|
|
222
|
+
},
|
|
223
|
+
"version": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"minLength": 1,
|
|
226
|
+
"description": "Protocol version"
|
|
227
|
+
},
|
|
228
|
+
"input": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": "Describes what the protocol accepts or produces"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://artifact-chain.dev/schemas/project-facts.schema.json",
|
|
4
|
+
"title": "Project Facts Evidence Envelope",
|
|
5
|
+
"description": "Structured envelope capturing project identity, configuration, graph state, and evidence for registry/planner consumption",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "projectRoot", "configDigest", "artifactGraphSummary", "targetArtifact", "contractRevisionDigest", "proofStatus", "versionLockStatus", "sourcesFreshness", "bindingFreshness", "evidenceDigest"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"type": "integer",
|
|
12
|
+
"const": 1
|
|
13
|
+
},
|
|
14
|
+
"projectRoot": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"minLength": 1,
|
|
17
|
+
"description": "Project root identity (path or canonical identifier)"
|
|
18
|
+
},
|
|
19
|
+
"configDigest": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^sha256:[a-f0-9]{64}$",
|
|
22
|
+
"description": "SHA-256 digest of artifact-graph.config.yaml content"
|
|
23
|
+
},
|
|
24
|
+
"policyDigest": {
|
|
25
|
+
"type": ["string", "null"],
|
|
26
|
+
"pattern": "^sha256:[a-f0-9]{64}$",
|
|
27
|
+
"description": "SHA-256 digest of project policy overlay, or null if none"
|
|
28
|
+
},
|
|
29
|
+
"artifactGraphSummary": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["artifactCount", "edgeCount", "contextTargets"],
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"properties": {
|
|
34
|
+
"artifactCount": { "type": "integer", "minimum": 0 },
|
|
35
|
+
"edgeCount": { "type": "integer", "minimum": 0 },
|
|
36
|
+
"contextTargets": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": { "type": "string" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"targetArtifact": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"required": ["type", "id"],
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"properties": {
|
|
47
|
+
"type": { "type": "string", "minLength": 1 },
|
|
48
|
+
"id": { "type": "string", "minLength": 1 }
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"contractRevisionDigest": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"pattern": "^sha256:[a-f0-9]{64}$",
|
|
54
|
+
"description": "SHA-256 digest of the contract revision content"
|
|
55
|
+
},
|
|
56
|
+
"proofStatus": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["present", "missing", "stale"]
|
|
59
|
+
},
|
|
60
|
+
"versionLockStatus": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"enum": ["fresh", "stale", "missing"]
|
|
63
|
+
},
|
|
64
|
+
"sourcesFreshness": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"enum": ["fresh", "stale", "missing"],
|
|
67
|
+
"description": "Freshness of the adopted catalog source set"
|
|
68
|
+
},
|
|
69
|
+
"bindingFreshness": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": ["fresh", "stale", "missing"],
|
|
72
|
+
"description": "Freshness of the project provider binding"
|
|
73
|
+
},
|
|
74
|
+
"evidenceDigest": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"pattern": "^sha256:[a-f0-9]{64}$",
|
|
77
|
+
"description": "SHA-256 digest of the envelope content excluding this field, using stable key serialization"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @feature ACA18 @scenario S-65 @decision D-ACA-18
|
|
3
|
+
// Deterministic Family API catalog validator.
|
|
4
|
+
// Validates all API files in family-apis/ and checks catalog consistency.
|
|
5
|
+
// JSON envelope output; non-zero exit on failure.
|
|
6
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import {
|
|
10
|
+
validateFamilyApi,
|
|
11
|
+
validateCatalogConsistency,
|
|
12
|
+
loadJson,
|
|
13
|
+
} from './lib/family-api-validator.mjs';
|
|
14
|
+
|
|
15
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
|
+
const familyApisDir = join(root, 'family-apis');
|
|
17
|
+
|
|
18
|
+
const errors = [];
|
|
19
|
+
const warnings = [];
|
|
20
|
+
const checked = [];
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// 1. Load catalog
|
|
24
|
+
const catalogPath = join(familyApisDir, 'catalog.json');
|
|
25
|
+
let catalog;
|
|
26
|
+
try {
|
|
27
|
+
catalog = await loadJson(catalogPath);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
errors.push(`Failed to load catalog.json: ${err.message}`);
|
|
30
|
+
emitResult();
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Discover API files
|
|
35
|
+
const apiFiles = [];
|
|
36
|
+
const familiesDir = join(familyApisDir);
|
|
37
|
+
for (const entry of await readdir(familiesDir, { withFileTypes: true })) {
|
|
38
|
+
if (!entry.isDirectory() || entry.name === 'schema') continue;
|
|
39
|
+
const apiPath = join(familiesDir, entry.name, 'api.json');
|
|
40
|
+
try {
|
|
41
|
+
const content = await loadJson(apiPath);
|
|
42
|
+
apiFiles.push({
|
|
43
|
+
path: `${entry.name}/api.json`,
|
|
44
|
+
id: content.api?.id,
|
|
45
|
+
major: content.api?.major,
|
|
46
|
+
content,
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Non-API directories are fine
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Validate each API file (async now)
|
|
54
|
+
for (const apiFile of apiFiles) {
|
|
55
|
+
const result = await validateFamilyApi(apiFile.content, { sourcePath: apiFile.path });
|
|
56
|
+
errors.push(...result.errors.map(e => typeof e === 'string' ? e : e.message || JSON.stringify(e)));
|
|
57
|
+
warnings.push(...result.warnings);
|
|
58
|
+
checked.push(apiFile.path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. Validate catalog consistency
|
|
62
|
+
const consistency = validateCatalogConsistency(
|
|
63
|
+
catalog,
|
|
64
|
+
apiFiles.map(f => ({ id: f.id, major: f.major, content: f.content })),
|
|
65
|
+
);
|
|
66
|
+
errors.push(...consistency.errors);
|
|
67
|
+
|
|
68
|
+
emitResult();
|
|
69
|
+
if (errors.length > 0) process.exit(1);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
errors.push(`Unexpected error: ${err.message}`);
|
|
72
|
+
emitResult();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function emitResult() {
|
|
77
|
+
const envelope = {
|
|
78
|
+
ok: errors.length === 0,
|
|
79
|
+
checked,
|
|
80
|
+
errors,
|
|
81
|
+
warnings,
|
|
82
|
+
};
|
|
83
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
84
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @feature ACA18 @scenario S-65 @decision D-ACA-18
|
|
3
|
+
// Authority Family API exporter. Outputs the exact bytes of a registered and
|
|
4
|
+
// validated Family API to a specified path. Supports --check mode for drift detection.
|
|
5
|
+
//
|
|
6
|
+
// CLI does not expose --catalog; only internal test injection can override it.
|
|
7
|
+
// All output is path-safe: no absolute paths in stdout or stderr.
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { loadJson, validateFamilyApi, computeRevisionDigest } from './lib/family-api-validator.mjs';
|
|
12
|
+
|
|
13
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Export a Family API from the authoritative catalog.
|
|
17
|
+
* @param {object} opts
|
|
18
|
+
* @param {string} opts.apiId - API id (e.g., "artifact.e2e-test-family")
|
|
19
|
+
* @param {number} opts.apiMajor - API major version
|
|
20
|
+
* @param {string} opts.outputPath - Absolute path to write the API JSON
|
|
21
|
+
* @param {boolean} [opts.check=false] - If true, only check for drift (exit nonzero on drift)
|
|
22
|
+
* @param {string} [opts.catalogPath] - Override catalog path (internal use only)
|
|
23
|
+
* @param {boolean} [opts.json=false] - If true, output structured JSON to stdout
|
|
24
|
+
* @returns {Promise<{ok: boolean, code: string, identity?: object, status?: string, digest?: string, message?: string}>}
|
|
25
|
+
*/
|
|
26
|
+
export async function exportFamilyApi({ apiId, apiMajor, outputPath, check = false, catalogPath, json = false }) {
|
|
27
|
+
// 1. Load and validate catalog
|
|
28
|
+
const catalogFilePath = catalogPath || join(root, 'family-apis', 'catalog.json');
|
|
29
|
+
if (!existsSync(catalogFilePath)) {
|
|
30
|
+
return { ok: false, code: 'CATALOG_NOT_FOUND', status: 'FAILED', message: 'Catalog file not found' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let catalog;
|
|
34
|
+
try {
|
|
35
|
+
catalog = await loadJson(catalogFilePath);
|
|
36
|
+
} catch {
|
|
37
|
+
return { ok: false, code: 'CATALOG_INVALID', status: 'FAILED', message: 'Catalog is not valid JSON' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!catalog?.families || !Array.isArray(catalog.families)) {
|
|
41
|
+
return { ok: false, code: 'CATALOG_INVALID', status: 'FAILED', message: 'Catalog missing families array' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Find matching family
|
|
45
|
+
const family = catalog.families.find(f => f.id === apiId && f.major === apiMajor);
|
|
46
|
+
if (!family) {
|
|
47
|
+
return { ok: false, code: 'API_NOT_REGISTERED', status: 'FAILED', message: 'API not registered in catalog' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Load and validate the API
|
|
51
|
+
const apiPath = join(root, 'family-apis', family.apiPath);
|
|
52
|
+
if (!existsSync(apiPath)) {
|
|
53
|
+
return { ok: false, code: 'API_FILE_MISSING', status: 'FAILED', message: 'API file missing from catalog path' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let apiContent;
|
|
57
|
+
try {
|
|
58
|
+
apiContent = readFileSync(apiPath, 'utf8');
|
|
59
|
+
} catch {
|
|
60
|
+
return { ok: false, code: 'API_READ_FAILED', status: 'FAILED', message: 'Cannot read API file' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let apiObject;
|
|
64
|
+
try {
|
|
65
|
+
apiObject = JSON.parse(apiContent);
|
|
66
|
+
} catch {
|
|
67
|
+
return { ok: false, code: 'API_PARSE_FAILED', status: 'FAILED', message: 'API file is not valid JSON' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate API schema and semantics
|
|
71
|
+
const validation = await validateFamilyApi(apiObject, { sourcePath: undefined });
|
|
72
|
+
if (!validation.ok) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false, code: 'API_VALIDATION_FAILED', status: 'FAILED',
|
|
75
|
+
message: `API validation failed: ${validation.errors.length} error(s)`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Verify revision digest matches catalog
|
|
80
|
+
const computedDigest = computeRevisionDigest(apiObject);
|
|
81
|
+
if (computedDigest !== family.revisionDigest) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false, code: 'REVISION_DIGEST_MISMATCH', status: 'FAILED',
|
|
84
|
+
message: 'Revision digest mismatch between catalog and computed value',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5. Check mode: compare with existing output
|
|
89
|
+
if (check) {
|
|
90
|
+
if (!existsSync(outputPath)) {
|
|
91
|
+
return { ok: false, code: 'DRIFT_MISSING_OUTPUT', status: 'DRIFT', message: 'Output file does not exist' };
|
|
92
|
+
}
|
|
93
|
+
let existing;
|
|
94
|
+
try {
|
|
95
|
+
existing = readFileSync(outputPath, 'utf8');
|
|
96
|
+
} catch {
|
|
97
|
+
return { ok: false, code: 'DRIFT_READ_FAILED', status: 'DRIFT', message: 'Cannot read output file' };
|
|
98
|
+
}
|
|
99
|
+
if (existing !== apiContent) {
|
|
100
|
+
return { ok: false, code: 'DRIFT_CONTENT_MISMATCH', status: 'DRIFT', message: 'Output differs from authority' };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
ok: true, code: 'CHECK_PASSED', status: 'NO_DRIFT',
|
|
104
|
+
identity: { apiId, apiMajor, revisionDigest: family.revisionDigest },
|
|
105
|
+
digest: computedDigest,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 6. Write mode: create parent dir, output exact bytes
|
|
110
|
+
try {
|
|
111
|
+
const parentDir = dirname(outputPath);
|
|
112
|
+
mkdirSync(parentDir, { recursive: true });
|
|
113
|
+
writeFileSync(outputPath, apiContent, 'utf8');
|
|
114
|
+
return {
|
|
115
|
+
ok: true, code: 'EXPORTED', status: 'EXPORTED',
|
|
116
|
+
identity: { apiId, apiMajor, revisionDigest: family.revisionDigest },
|
|
117
|
+
digest: computedDigest,
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return { ok: false, code: 'WRITE_FAILED', status: 'FAILED', message: 'Cannot write output file' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// CLI entry point
|
|
125
|
+
if (process.argv[1] && process.argv[1].endsWith('export-family-api.mjs')) {
|
|
126
|
+
const args = process.argv.slice(2);
|
|
127
|
+
const getArg = (name) => {
|
|
128
|
+
for (let i = 0; i < args.length; i++) {
|
|
129
|
+
if (args[i] === `--${name}` && i + 1 < args.length) return args[i + 1];
|
|
130
|
+
if (args[i].startsWith(`--${name}=`)) return args[i].slice(`--${name}=`.length);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const apiId = getArg('api-id');
|
|
136
|
+
const apiMajorStr = getArg('api-major');
|
|
137
|
+
const output = getArg('output');
|
|
138
|
+
const check = args.includes('--check');
|
|
139
|
+
const jsonMode = args.includes('--json');
|
|
140
|
+
// --catalog is internal-only, not in public usage help
|
|
141
|
+
|
|
142
|
+
if (!apiId || !apiMajorStr || !output) {
|
|
143
|
+
if (jsonMode) {
|
|
144
|
+
process.stdout.write(JSON.stringify({ ok: false, code: 'USAGE', status: 'FAILED', message: 'Missing required arguments' }) + '\n');
|
|
145
|
+
} else {
|
|
146
|
+
process.stderr.write('Usage: export-family-api.mjs --api-id=<id> --api-major=<n> --output=<path> [--check] [--json]\n');
|
|
147
|
+
}
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const apiMajor = parseInt(apiMajorStr, 10);
|
|
152
|
+
if (isNaN(apiMajor)) {
|
|
153
|
+
if (jsonMode) {
|
|
154
|
+
process.stdout.write(JSON.stringify({ ok: false, code: 'INVALID_ARGUMENT', status: 'FAILED', message: '--api-major must be a number' }) + '\n');
|
|
155
|
+
} else {
|
|
156
|
+
process.stderr.write('--api-major must be a number\n');
|
|
157
|
+
}
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --catalog is only available via env var for testing (not a public CLI flag)
|
|
162
|
+
const catalogPath = getArg('catalog');
|
|
163
|
+
const result = await exportFamilyApi({ apiId, apiMajor, outputPath: output, check, catalogPath, json: jsonMode });
|
|
164
|
+
|
|
165
|
+
if (jsonMode) {
|
|
166
|
+
// JSON output: stable fields only, no absolute paths
|
|
167
|
+
process.stdout.write(JSON.stringify({
|
|
168
|
+
ok: result.ok,
|
|
169
|
+
code: result.code,
|
|
170
|
+
status: result.status,
|
|
171
|
+
...(result.identity ? { identity: result.identity } : {}),
|
|
172
|
+
...(result.digest ? { digest: result.digest } : {}),
|
|
173
|
+
...(result.message ? { message: result.message } : {}),
|
|
174
|
+
}, null, 2) + '\n');
|
|
175
|
+
} else {
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
process.stderr.write(`ERROR: ${result.code}\n`);
|
|
178
|
+
} else {
|
|
179
|
+
process.stdout.write(`${result.code}: ${result.status}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.exit(result.ok ? 0 : 1);
|
|
184
|
+
}
|