phasegate 0.229.0 → 0.254.0
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/CHANGELOG.md +51 -1
- package/README.ja.md +28 -1
- package/README.md +27 -0
- package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
- package/docs/ADR/032-world-node-identity.md +198 -0
- package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
- package/docs/ADR/034-world-constraint-semantics.md +218 -0
- package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
- package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
- package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
- package/docs/contracts/requirement-test-matrix.schema.json +15 -0
- package/docs/contracts/world-baseline.schema.json +35 -0
- package/docs/contracts/world-constraints.schema.json +56 -0
- package/docs/contracts/world-debts.schema.json +32 -0
- package/docs/contracts/world-obligation-report.schema.json +259 -0
- package/docs/contracts/world-waivers.schema.json +32 -0
- package/docs/guide/cli-reference.md +34 -0
- package/package.json +1 -1
- package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
- package/scripts/harness/attestation/composition-root.ts +14 -4
- package/scripts/harness/attestation/index.ts +6 -1
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
- package/scripts/harness/main.ts +107 -1
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
- package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
- package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
- package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
- package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
- package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
- package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
- package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
- package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
- package/scripts/harness/traceability-model/composition-root.ts +21 -5
- package/scripts/harness/traceability-model/index.ts +17 -6
- package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
- package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
- package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
- package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +62 -0
- package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
- package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
- package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
- package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
- package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
- package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +144 -0
- package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
- package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
- package/scripts/harness/world-model/composition-root.ts +218 -0
- package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
- package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
- package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
- package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
- package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
- package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
- package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
- package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
- package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
- package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
- package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
- package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
- package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
- package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
- package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
- package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
- package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
- package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
- package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
- package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
- package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
- package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
- package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
- package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
- package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
- package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
- package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
- package/scripts/harness/world-model/index.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
- package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +288 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
- package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
- package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
- package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
- package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
- package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +330 -0
- package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
- package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
- package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
- package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
- package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
- package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
- package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
- package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
- package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
- package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
- package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
- package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
- package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
- package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +105 -0
- package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
- package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-294
|
|
4
|
+
|
|
5
|
+
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import Ajv2020Module, { type ErrorObject } from "ajv/dist/2020.js";
|
|
9
|
+
import type {
|
|
10
|
+
AdoptionBaselineRepositoryPort,
|
|
11
|
+
ConstraintDeclarationRepositoryPort,
|
|
12
|
+
ConstraintDeclarationSet,
|
|
13
|
+
SemanticDebtRepositoryPort,
|
|
14
|
+
WaiverDeclarationRepositoryPort,
|
|
15
|
+
WorldControlDiagnosticDto,
|
|
16
|
+
WorldControlReadResult,
|
|
17
|
+
WorldControlReplaceResult,
|
|
18
|
+
} from "../../application/ports/world-control-declaration-repository-port.js";
|
|
19
|
+
import type {
|
|
20
|
+
AdoptionBaseline,
|
|
21
|
+
SemanticDebtDeclaration,
|
|
22
|
+
WorldWaiver,
|
|
23
|
+
} from "../../domain/entities/control-declarations.js";
|
|
24
|
+
import { ArtifactKind } from "../../domain/value-objects/artifact-kind.js";
|
|
25
|
+
import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
|
|
26
|
+
import { PathKey } from "../../domain/value-objects/path-key.js";
|
|
27
|
+
import { WorldNodeId } from "../../domain/value-objects/world-node-id.js";
|
|
28
|
+
import {
|
|
29
|
+
InvalidControlDeclarationError,
|
|
30
|
+
mapAdoptionBaselineDocument,
|
|
31
|
+
mapConstraintDeclarationDocument,
|
|
32
|
+
mapSemanticDebtDocument,
|
|
33
|
+
mapWaiverDocument,
|
|
34
|
+
} from "./world-control-declaration-mapper.js";
|
|
35
|
+
|
|
36
|
+
const Ajv2020 = Ajv2020Module.default ?? Ajv2020Module;
|
|
37
|
+
|
|
38
|
+
const CONTRACT_ROOT = fileURLToPath(new URL("../../../../../docs/contracts/", import.meta.url));
|
|
39
|
+
let temporarySequence = 0;
|
|
40
|
+
|
|
41
|
+
interface RepositoryOptions {
|
|
42
|
+
readonly rootDir: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DeclarationSpec<T> {
|
|
46
|
+
readonly fileName: string;
|
|
47
|
+
readonly schemaFileName: string;
|
|
48
|
+
readonly schemaVersion: string;
|
|
49
|
+
readonly emptyValue: T;
|
|
50
|
+
readonly map: (value: unknown) => T;
|
|
51
|
+
readonly allowMalformedConstraintRecords?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type RawReadResult =
|
|
55
|
+
| { readonly state: "absent" }
|
|
56
|
+
| { readonly state: "present"; readonly value: unknown }
|
|
57
|
+
| { readonly state: "invalid"; readonly diagnostic: WorldControlDiagnosticDto };
|
|
58
|
+
|
|
59
|
+
const freezeDiagnostic = (
|
|
60
|
+
code: string,
|
|
61
|
+
fileName: string,
|
|
62
|
+
message: string,
|
|
63
|
+
locator: string | null = null,
|
|
64
|
+
): WorldControlDiagnosticDto => Object.freeze({ code, path: fileName, locator, message });
|
|
65
|
+
|
|
66
|
+
const schemaVersionOf = (value: unknown): unknown =>
|
|
67
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
68
|
+
? (value as Record<string, unknown>).schemaVersion
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
const formatAjvErrors = (errors: readonly ErrorObject[] | null | undefined): string =>
|
|
72
|
+
(errors ?? [])
|
|
73
|
+
.map((error) => `${error.instancePath || "/"} ${error.message ?? error.keyword}`)
|
|
74
|
+
.sort()
|
|
75
|
+
.join("; ");
|
|
76
|
+
|
|
77
|
+
const relaxedConstraintEnvelopeSchema = (schema: Record<string, unknown>): Record<string, unknown> => {
|
|
78
|
+
const copy = structuredClone(schema) as Record<string, unknown>;
|
|
79
|
+
const properties = copy.properties as Record<string, unknown>;
|
|
80
|
+
const constraints = properties.constraints as Record<string, unknown>;
|
|
81
|
+
const aliases = properties.aliases as Record<string, unknown>;
|
|
82
|
+
constraints.items = { type: "object" };
|
|
83
|
+
aliases.items = { type: "object" };
|
|
84
|
+
return copy;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
class FileSystemWorldControlRepository<T> {
|
|
88
|
+
constructor(
|
|
89
|
+
private readonly options: RepositoryOptions,
|
|
90
|
+
private readonly spec: DeclarationSpec<T>,
|
|
91
|
+
) {}
|
|
92
|
+
|
|
93
|
+
async load(): Promise<WorldControlReadResult<T>> {
|
|
94
|
+
const raw = await this.readRaw();
|
|
95
|
+
if (raw.state === "absent") {
|
|
96
|
+
return Object.freeze({ state: "absent", value: this.spec.emptyValue, diagnostics: Object.freeze([]) });
|
|
97
|
+
}
|
|
98
|
+
if (raw.state === "invalid") {
|
|
99
|
+
return Object.freeze({ state: "invalid", diagnostics: Object.freeze([raw.diagnostic]) });
|
|
100
|
+
}
|
|
101
|
+
const admitted = await this.admit(raw.value, this.spec.allowMalformedConstraintRecords === true);
|
|
102
|
+
if (admitted.state === "invalid") return admitted;
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
state: "loaded",
|
|
105
|
+
value: admitted.value,
|
|
106
|
+
diagnostics: Object.freeze(this.isConstraintSet(admitted.value) ? [...admitted.value.diagnostics] : []),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async replaceAtomically(document: unknown): Promise<WorldControlReplaceResult> {
|
|
111
|
+
const admitted = await this.admit(document, false);
|
|
112
|
+
if (admitted.state === "invalid") return admitted;
|
|
113
|
+
const absolutePath = path.join(this.options.rootDir, this.spec.fileName);
|
|
114
|
+
const temporaryPath = `${absolutePath}.${process.pid}-${temporarySequence++}.tmp`;
|
|
115
|
+
try {
|
|
116
|
+
await writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
117
|
+
await rename(temporaryPath, absolutePath);
|
|
118
|
+
return Object.freeze({ state: "written", path: this.spec.fileName });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
state: "invalid",
|
|
123
|
+
diagnostics: Object.freeze([
|
|
124
|
+
freezeDiagnostic(
|
|
125
|
+
"control-write-failure",
|
|
126
|
+
this.spec.fileName,
|
|
127
|
+
error instanceof Error ? error.message : String(error),
|
|
128
|
+
),
|
|
129
|
+
]),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async readRaw(): Promise<RawReadResult> {
|
|
135
|
+
let bytes: Uint8Array;
|
|
136
|
+
try {
|
|
137
|
+
bytes = await readFile(path.join(this.options.rootDir, this.spec.fileName));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { state: "absent" };
|
|
140
|
+
return {
|
|
141
|
+
state: "invalid",
|
|
142
|
+
diagnostic: freezeDiagnostic(
|
|
143
|
+
"control-read-failure",
|
|
144
|
+
this.spec.fileName,
|
|
145
|
+
error instanceof Error ? error.message : String(error),
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
let text: string;
|
|
150
|
+
try {
|
|
151
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
152
|
+
} catch {
|
|
153
|
+
return {
|
|
154
|
+
state: "invalid",
|
|
155
|
+
diagnostic: freezeDiagnostic("invalid-utf8", this.spec.fileName, "Control file is not valid UTF-8"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return { state: "present", value: JSON.parse(text) };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
state: "invalid",
|
|
163
|
+
diagnostic: freezeDiagnostic(
|
|
164
|
+
"invalid-json",
|
|
165
|
+
this.spec.fileName,
|
|
166
|
+
error instanceof Error ? error.message : String(error),
|
|
167
|
+
),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async admit(
|
|
173
|
+
value: unknown,
|
|
174
|
+
allowMalformedConstraintRecords: boolean,
|
|
175
|
+
): Promise<WorldControlReadResult<T> & { readonly state: "loaded" | "invalid" }> {
|
|
176
|
+
const schemaVersion = schemaVersionOf(value);
|
|
177
|
+
if (schemaVersion === undefined) {
|
|
178
|
+
return this.invalid("missing-schema-version", "schemaVersion is required");
|
|
179
|
+
}
|
|
180
|
+
if (schemaVersion !== this.spec.schemaVersion) {
|
|
181
|
+
return this.invalid(
|
|
182
|
+
"unsupported-schema-version",
|
|
183
|
+
`Expected ${this.spec.schemaVersion}, received ${String(schemaVersion)}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
let schema: Record<string, unknown>;
|
|
187
|
+
try {
|
|
188
|
+
schema = JSON.parse(await readFile(path.join(CONTRACT_ROOT, this.spec.schemaFileName), "utf8")) as Record<
|
|
189
|
+
string,
|
|
190
|
+
unknown
|
|
191
|
+
>;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return this.invalid("schema-load-failure", error instanceof Error ? error.message : String(error));
|
|
194
|
+
}
|
|
195
|
+
const validationSchema = allowMalformedConstraintRecords ? relaxedConstraintEnvelopeSchema(schema) : schema;
|
|
196
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
197
|
+
const validate = ajv.compile(validationSchema);
|
|
198
|
+
if (!validate(value)) {
|
|
199
|
+
return this.invalid("malformed-control-document", formatAjvErrors(validate.errors));
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return Object.freeze({ state: "loaded", value: this.spec.map(value), diagnostics: Object.freeze([]) });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
+
return this.invalid(
|
|
206
|
+
error instanceof InvalidControlDeclarationError ? "malformed-control-document" : "control-mapping-failure",
|
|
207
|
+
message,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private invalid(
|
|
213
|
+
code: string,
|
|
214
|
+
message: string,
|
|
215
|
+
): { readonly state: "invalid"; readonly diagnostics: readonly WorldControlDiagnosticDto[] } {
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
state: "invalid",
|
|
218
|
+
diagnostics: Object.freeze([freezeDiagnostic(code, this.spec.fileName, message)]),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private isConstraintSet(value: T): value is T & ConstraintDeclarationSet {
|
|
223
|
+
return typeof value === "object" && value !== null && "malformedDeclarations" in value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const emptyConstraints = (): ConstraintDeclarationSet =>
|
|
228
|
+
Object.freeze({
|
|
229
|
+
schemaVersion: "phasegate-world-constraints/v1",
|
|
230
|
+
records: Object.freeze([]),
|
|
231
|
+
malformedDeclarations: Object.freeze([]),
|
|
232
|
+
aliases: Object.freeze([]),
|
|
233
|
+
relations: Object.freeze([]),
|
|
234
|
+
diagnostics: Object.freeze([]),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export class FileSystemConstraintDeclarationRepositoryAdapter implements ConstraintDeclarationRepositoryPort {
|
|
238
|
+
private readonly repository: FileSystemWorldControlRepository<ConstraintDeclarationSet>;
|
|
239
|
+
|
|
240
|
+
constructor(options: RepositoryOptions) {
|
|
241
|
+
const fileName = "phasegate.world-constraints.json";
|
|
242
|
+
const artifactId = WorldNodeId.artifact(
|
|
243
|
+
ArtifactKind.externalDeclaration(),
|
|
244
|
+
CorpusRole.external(),
|
|
245
|
+
PathKey.create(fileName),
|
|
246
|
+
);
|
|
247
|
+
this.repository = new FileSystemWorldControlRepository(options, {
|
|
248
|
+
fileName,
|
|
249
|
+
schemaFileName: "world-constraints.schema.json",
|
|
250
|
+
schemaVersion: "phasegate-world-constraints/v1",
|
|
251
|
+
emptyValue: emptyConstraints(),
|
|
252
|
+
map: (value) => mapConstraintDeclarationDocument(value, artifactId),
|
|
253
|
+
allowMalformedConstraintRecords: true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
load(): Promise<WorldControlReadResult<ConstraintDeclarationSet>> {
|
|
258
|
+
return this.repository.load();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
replaceAtomically(document: unknown): Promise<WorldControlReplaceResult> {
|
|
262
|
+
return this.repository.replaceAtomically(document);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export class FileSystemAdoptionBaselineRepositoryAdapter implements AdoptionBaselineRepositoryPort {
|
|
267
|
+
private readonly repository: FileSystemWorldControlRepository<AdoptionBaseline | null>;
|
|
268
|
+
|
|
269
|
+
constructor(options: RepositoryOptions) {
|
|
270
|
+
this.repository = new FileSystemWorldControlRepository(options, {
|
|
271
|
+
fileName: "phasegate.world-baseline.json",
|
|
272
|
+
schemaFileName: "world-baseline.schema.json",
|
|
273
|
+
schemaVersion: "phasegate-world-adoption-baseline/v1",
|
|
274
|
+
emptyValue: null,
|
|
275
|
+
map: mapAdoptionBaselineDocument,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
load(): Promise<WorldControlReadResult<AdoptionBaseline | null>> {
|
|
280
|
+
return this.repository.load();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
replaceAtomically(document: unknown): Promise<WorldControlReplaceResult> {
|
|
284
|
+
return this.repository.replaceAtomically(document);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export class FileSystemWaiverDeclarationRepositoryAdapter implements WaiverDeclarationRepositoryPort {
|
|
289
|
+
private readonly repository: FileSystemWorldControlRepository<readonly WorldWaiver[]>;
|
|
290
|
+
|
|
291
|
+
constructor(options: RepositoryOptions) {
|
|
292
|
+
this.repository = new FileSystemWorldControlRepository(options, {
|
|
293
|
+
fileName: "phasegate.world-waivers.json",
|
|
294
|
+
schemaFileName: "world-waivers.schema.json",
|
|
295
|
+
schemaVersion: "phasegate-world-waivers/v1",
|
|
296
|
+
emptyValue: Object.freeze([]),
|
|
297
|
+
map: mapWaiverDocument,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
load(): Promise<WorldControlReadResult<readonly WorldWaiver[]>> {
|
|
302
|
+
return this.repository.load();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
replaceAtomically(document: unknown): Promise<WorldControlReplaceResult> {
|
|
306
|
+
return this.repository.replaceAtomically(document);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export class FileSystemSemanticDebtRepositoryAdapter implements SemanticDebtRepositoryPort {
|
|
311
|
+
private readonly repository: FileSystemWorldControlRepository<readonly SemanticDebtDeclaration[]>;
|
|
312
|
+
|
|
313
|
+
constructor(options: RepositoryOptions) {
|
|
314
|
+
this.repository = new FileSystemWorldControlRepository(options, {
|
|
315
|
+
fileName: "phasegate.world-debts.json",
|
|
316
|
+
schemaFileName: "world-debts.schema.json",
|
|
317
|
+
schemaVersion: "phasegate-world-debts/v1",
|
|
318
|
+
emptyValue: Object.freeze([]),
|
|
319
|
+
map: mapSemanticDebtDocument,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
load(): Promise<WorldControlReadResult<readonly SemanticDebtDeclaration[]>> {
|
|
324
|
+
return this.repository.load();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
replaceAtomically(document: unknown): Promise<WorldControlReplaceResult> {
|
|
328
|
+
return this.repository.replaceAtomically(document);
|
|
329
|
+
}
|
|
330
|
+
}
|
package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-290
|
|
4
|
+
|
|
5
|
+
import { WorldNode } from "../../domain/entities/world-node.js";
|
|
6
|
+
import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
|
|
7
|
+
import { type CanonicalJsonObject, CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
|
|
8
|
+
import { ArtifactKind } from "../../domain/value-objects/artifact-kind.js";
|
|
9
|
+
import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
|
|
10
|
+
import { PathKey } from "../../domain/value-objects/path-key.js";
|
|
11
|
+
import {
|
|
12
|
+
assertExactKeys,
|
|
13
|
+
OwnerProjectionError,
|
|
14
|
+
projectionDiagnostic,
|
|
15
|
+
readOptionalJson,
|
|
16
|
+
requireObject,
|
|
17
|
+
requireString,
|
|
18
|
+
} from "./json-fact-extractor-support.js";
|
|
19
|
+
import type { RuntimeFactExtraction } from "./runtime-fact-extraction.js";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_INTEGRITY_PATH = "phasegate.integrity.json";
|
|
22
|
+
const RAW_SHA256 = /^[0-9a-f]{64}$/;
|
|
23
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
24
|
+
|
|
25
|
+
interface IntegrityProjection {
|
|
26
|
+
readonly version: 1;
|
|
27
|
+
readonly algorithm: "sha256";
|
|
28
|
+
readonly declarations: readonly {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly digest: string;
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IntegrityManifestFactExtractorDeps {
|
|
35
|
+
readonly rootDir: string;
|
|
36
|
+
readonly hashingPort: WorldHashingPort;
|
|
37
|
+
readonly manifestPath?: string;
|
|
38
|
+
readonly serializer?: CanonicalJsonSerializer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class IntegrityManifestFactExtractor {
|
|
42
|
+
private readonly deps: IntegrityManifestFactExtractorDeps;
|
|
43
|
+
private readonly serializer: CanonicalJsonSerializer;
|
|
44
|
+
|
|
45
|
+
constructor(deps: IntegrityManifestFactExtractorDeps) {
|
|
46
|
+
this.deps = deps;
|
|
47
|
+
this.serializer = deps.serializer ?? new CanonicalJsonSerializer();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async extract(): Promise<RuntimeFactExtraction> {
|
|
51
|
+
const relativePath = this.deps.manifestPath ?? DEFAULT_INTEGRITY_PATH;
|
|
52
|
+
const read = await readOptionalJson(this.deps.rootDir, relativePath, "integrity");
|
|
53
|
+
if (read.state !== "present") {
|
|
54
|
+
return { nodes: [], edges: [], diagnostics: [read.diagnostic] };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const projection = this.project(read.value);
|
|
58
|
+
const artifact = WorldNode.artifact({
|
|
59
|
+
artifactKind: ArtifactKind.externalDeclaration(),
|
|
60
|
+
corpusRole: CorpusRole.external(),
|
|
61
|
+
path: read.path,
|
|
62
|
+
digest: this.deps.hashingPort.sha256(this.serializer.serialize(projection)),
|
|
63
|
+
attributes: projection as unknown as CanonicalJsonObject,
|
|
64
|
+
});
|
|
65
|
+
return { nodes: [artifact], edges: [], diagnostics: [] };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
nodes: [],
|
|
69
|
+
edges: [],
|
|
70
|
+
diagnostics: [projectionDiagnostic(read.path, "integrity", error)],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private project(value: unknown): IntegrityProjection {
|
|
76
|
+
const root = requireObject(value, "integrity");
|
|
77
|
+
assertExactKeys(root, ["version", "algorithm", "files"], "integrity");
|
|
78
|
+
if (root.version !== 1 || root.algorithm !== "sha256") {
|
|
79
|
+
throw new OwnerProjectionError("unsupported-provider-schema", "unsupported integrity schema", {
|
|
80
|
+
algorithm: typeof root.algorithm === "string" ? root.algorithm : null,
|
|
81
|
+
version: typeof root.version === "number" ? root.version : -1,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const files = requireObject(root.files, "integrity.files");
|
|
85
|
+
const declarations = Object.entries(files).map(([filePath, rawDigest]) => {
|
|
86
|
+
PathKey.create(filePath);
|
|
87
|
+
const digest = requireString(rawDigest, `integrity.files.${filePath}`);
|
|
88
|
+
if (!RAW_SHA256.test(digest)) {
|
|
89
|
+
throw new OwnerProjectionError("malformed-provider-document", `integrity digest is invalid for ${filePath}`, {
|
|
90
|
+
filePath,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return { path: filePath, digest: `sha256:${digest}` };
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
version: 1,
|
|
97
|
+
algorithm: "sha256",
|
|
98
|
+
declarations: declarations.sort((left, right) => compareStrings(left.path, right.path)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-290
|
|
4
|
+
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
|
|
8
|
+
import { PathKey } from "../../domain/value-objects/path-key.js";
|
|
9
|
+
|
|
10
|
+
export class OwnerProjectionError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly payload: Readonly<Record<string, string | number | boolean | null | readonly string[]>>;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
code: string,
|
|
16
|
+
message: string,
|
|
17
|
+
payload: Record<string, string | number | boolean | null | readonly string[]> = {},
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "OwnerProjectionError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.payload = Object.freeze({ ...payload });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type OptionalJsonReadResult =
|
|
27
|
+
| { readonly state: "present"; readonly value: unknown; readonly path: PathKey }
|
|
28
|
+
| { readonly state: "not-present"; readonly diagnostic: ExtractionDiagnostic }
|
|
29
|
+
| { readonly state: "invalid"; readonly diagnostic: ExtractionDiagnostic };
|
|
30
|
+
|
|
31
|
+
export const readOptionalJson = async (
|
|
32
|
+
rootDir: string,
|
|
33
|
+
relativePath: string,
|
|
34
|
+
provider: string,
|
|
35
|
+
): Promise<OptionalJsonReadResult> => {
|
|
36
|
+
const pathKey = PathKey.create(relativePath);
|
|
37
|
+
let bytes: Uint8Array;
|
|
38
|
+
try {
|
|
39
|
+
bytes = await readFile(path.join(rootDir, relativePath));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
42
|
+
return {
|
|
43
|
+
state: "not-present",
|
|
44
|
+
diagnostic: ExtractionDiagnostic.create({
|
|
45
|
+
code: "not-present",
|
|
46
|
+
path: pathKey,
|
|
47
|
+
payload: { provider },
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
state: "invalid",
|
|
53
|
+
diagnostic: ExtractionDiagnostic.create({
|
|
54
|
+
code: "provider-read-failure",
|
|
55
|
+
path: pathKey,
|
|
56
|
+
payload: {
|
|
57
|
+
message: error instanceof Error ? error.message : String(error),
|
|
58
|
+
provider,
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let text: string;
|
|
65
|
+
try {
|
|
66
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
67
|
+
} catch {
|
|
68
|
+
return {
|
|
69
|
+
state: "invalid",
|
|
70
|
+
diagnostic: ExtractionDiagnostic.create({
|
|
71
|
+
code: "invalid-utf8",
|
|
72
|
+
path: pathKey,
|
|
73
|
+
payload: { byteLength: bytes.byteLength, provider },
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return { state: "present", value: JSON.parse(text), path: pathKey };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
state: "invalid",
|
|
82
|
+
diagnostic: ExtractionDiagnostic.create({
|
|
83
|
+
code: "provider-parse-failure",
|
|
84
|
+
path: pathKey,
|
|
85
|
+
payload: {
|
|
86
|
+
message: error instanceof Error ? error.message : String(error),
|
|
87
|
+
provider,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const projectionDiagnostic = (pathKey: PathKey, provider: string, error: unknown): ExtractionDiagnostic => {
|
|
95
|
+
if (error instanceof OwnerProjectionError) {
|
|
96
|
+
return ExtractionDiagnostic.create({
|
|
97
|
+
code: error.code,
|
|
98
|
+
path: pathKey,
|
|
99
|
+
payload: { ...error.payload, message: error.message, provider },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return ExtractionDiagnostic.create({
|
|
103
|
+
code: "provider-projection-failure",
|
|
104
|
+
path: pathKey,
|
|
105
|
+
payload: {
|
|
106
|
+
message: error instanceof Error ? error.message : String(error),
|
|
107
|
+
provider,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const requireObject = (value: unknown, field: string): Record<string, unknown> => {
|
|
113
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
114
|
+
throw new OwnerProjectionError("malformed-provider-document", `${field} must be an object`, { field });
|
|
115
|
+
}
|
|
116
|
+
return value as Record<string, unknown>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const requireArray = (value: unknown, field: string): readonly unknown[] => {
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
throw new OwnerProjectionError("malformed-provider-document", `${field} must be an array`, { field });
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const requireString = (value: unknown, field: string): string => {
|
|
127
|
+
if (typeof value !== "string") {
|
|
128
|
+
throw new OwnerProjectionError("malformed-provider-document", `${field} must be a string`, { field });
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const requireBoolean = (value: unknown, field: string): boolean => {
|
|
134
|
+
if (typeof value !== "boolean") {
|
|
135
|
+
throw new OwnerProjectionError("malformed-provider-document", `${field} must be a boolean`, { field });
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const assertExactKeys = (value: Record<string, unknown>, allowed: readonly string[], field: string): void => {
|
|
141
|
+
const unsupported = Object.keys(value)
|
|
142
|
+
.filter((key) => !allowed.includes(key))
|
|
143
|
+
.sort();
|
|
144
|
+
if (unsupported.length > 0) {
|
|
145
|
+
throw new OwnerProjectionError(
|
|
146
|
+
"unsupported-projection-field",
|
|
147
|
+
`${field} contains unsupported fields: ${unsupported.join(", ")}`,
|
|
148
|
+
{ field, unsupported },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
};
|