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.
Files changed (106) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.ja.md +28 -1
  3. package/README.md +27 -0
  4. package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
  5. package/docs/ADR/032-world-node-identity.md +198 -0
  6. package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
  7. package/docs/ADR/034-world-constraint-semantics.md +218 -0
  8. package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
  9. package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
  10. package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
  11. package/docs/contracts/requirement-test-matrix.schema.json +15 -0
  12. package/docs/contracts/world-baseline.schema.json +35 -0
  13. package/docs/contracts/world-constraints.schema.json +56 -0
  14. package/docs/contracts/world-debts.schema.json +32 -0
  15. package/docs/contracts/world-obligation-report.schema.json +259 -0
  16. package/docs/contracts/world-waivers.schema.json +32 -0
  17. package/docs/guide/cli-reference.md +34 -0
  18. package/package.json +1 -1
  19. package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
  20. package/scripts/harness/attestation/composition-root.ts +14 -4
  21. package/scripts/harness/attestation/index.ts +6 -1
  22. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
  23. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
  24. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
  25. package/scripts/harness/main.ts +107 -1
  26. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
  27. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
  28. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
  29. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
  30. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
  31. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
  32. package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
  33. package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
  34. package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
  35. package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
  36. package/scripts/harness/traceability-model/composition-root.ts +21 -5
  37. package/scripts/harness/traceability-model/index.ts +17 -6
  38. package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
  39. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
  40. package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
  41. package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
  42. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +62 -0
  43. package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
  44. package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
  45. package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
  46. package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
  47. package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
  48. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +144 -0
  49. package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
  50. package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
  51. package/scripts/harness/world-model/composition-root.ts +218 -0
  52. package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
  53. package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
  54. package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
  55. package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
  56. package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
  57. package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
  58. package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
  59. package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
  60. package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
  61. package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
  62. package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
  63. package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
  64. package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
  65. package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
  66. package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
  67. package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
  68. package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
  69. package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
  70. package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
  71. package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
  72. package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
  73. package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
  74. package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
  75. package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
  76. package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
  77. package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
  78. package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
  79. package/scripts/harness/world-model/index.ts +22 -0
  80. package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
  81. package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
  82. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +288 -0
  83. package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
  84. package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
  85. package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
  86. package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
  87. package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
  88. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +330 -0
  89. package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
  90. package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
  91. package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
  92. package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
  93. package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
  94. package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
  95. package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
  96. package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
  97. package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
  98. package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
  99. package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
  100. package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
  101. package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
  102. package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
  103. package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
  104. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +105 -0
  105. package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
  106. package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
@@ -0,0 +1,111 @@
1
+ // @unit world-model
2
+ // @layer application
3
+ // @work-item-id WI-291
4
+
5
+ import type { Edge } from "../../domain/entities/edge.js";
6
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
7
+ import type { Snapshot } from "../../domain/entities/snapshot.js";
8
+ import type { WorldNode } from "../../domain/entities/world-node.js";
9
+ import type { CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
10
+ import type { SnapshotRootDeriver } from "../../domain/services/snapshot-root-deriver.js";
11
+ import type { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
12
+ import type { WorldFactSourcePort } from "../ports/world-fact-source-port.js";
13
+
14
+ export interface BuildSnapshotContract {
15
+ execute(): Promise<Snapshot>;
16
+ }
17
+
18
+ export interface BuildSnapshotUseCaseDeps {
19
+ readonly factSource: WorldFactSourcePort;
20
+ readonly rootDeriver: SnapshotRootDeriver;
21
+ readonly serializer: CanonicalJsonSerializer;
22
+ readonly schemaVersion: string;
23
+ readonly extractorVersion: string;
24
+ readonly corpusConfigDigest: Sha256Digest;
25
+ }
26
+
27
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
28
+
29
+ export class BuildSnapshotUseCase implements BuildSnapshotContract {
30
+ constructor(private readonly deps: BuildSnapshotUseCaseDeps) {}
31
+
32
+ async execute(): Promise<Snapshot> {
33
+ const batches = await this.deps.factSource.extract();
34
+ const diagnostics = batches.flatMap((batch) => batch.diagnostics);
35
+ const nodes = this.admitNodes(
36
+ batches.flatMap((batch) => batch.nodes),
37
+ diagnostics,
38
+ );
39
+ const edges = this.admitEdges(
40
+ batches.flatMap((batch) => batch.edges),
41
+ new Set(nodes.map((node) => node.id.toString())),
42
+ diagnostics,
43
+ );
44
+ return this.deps.rootDeriver.buildSnapshot({
45
+ schemaVersion: this.deps.schemaVersion,
46
+ extractorVersion: this.deps.extractorVersion,
47
+ corpusConfigDigest: this.deps.corpusConfigDigest,
48
+ nodes,
49
+ edges,
50
+ extractionDiagnostics: diagnostics,
51
+ });
52
+ }
53
+
54
+ private admitNodes(candidates: readonly WorldNode[], diagnostics: ExtractionDiagnostic[]): readonly WorldNode[] {
55
+ const grouped = new Map<string, WorldNode[]>();
56
+ for (const candidate of candidates) {
57
+ const key = candidate.id.toString();
58
+ grouped.set(key, [...(grouped.get(key) ?? []), candidate]);
59
+ }
60
+ const admitted: WorldNode[] = [];
61
+ for (const [nodeId, entries] of [...grouped.entries()].sort(([left], [right]) => compareStrings(left, right))) {
62
+ if (entries.length === 1) {
63
+ admitted.push(entries[0]);
64
+ continue;
65
+ }
66
+ diagnostics.push(
67
+ ExtractionDiagnostic.create({
68
+ code: "duplicate-node-id",
69
+ nodeId: entries[0].id,
70
+ payload: {
71
+ candidates: entries.length,
72
+ candidateFacts: entries
73
+ .map((entry) => this.deps.serializer.stringify(entry.toCanonicalValue()))
74
+ .sort(compareStrings),
75
+ nodeId,
76
+ },
77
+ }),
78
+ );
79
+ }
80
+ return admitted;
81
+ }
82
+
83
+ private admitEdges(
84
+ candidates: readonly Edge[],
85
+ nodeIds: ReadonlySet<string>,
86
+ diagnostics: ExtractionDiagnostic[],
87
+ ): readonly Edge[] {
88
+ const unique = new Map<string, Edge>();
89
+ for (const edge of candidates) {
90
+ const key = this.deps.serializer.stringify(edge.toCanonicalValue());
91
+ if (!unique.has(key)) unique.set(key, edge);
92
+ }
93
+ const admitted: Edge[] = [];
94
+ for (const [edgeKey, edge] of [...unique.entries()].sort(([left], [right]) => compareStrings(left, right))) {
95
+ const missingEndpoints = [edge.from.toString(), edge.to.toString()]
96
+ .filter((endpoint) => !nodeIds.has(endpoint))
97
+ .sort(compareStrings);
98
+ if (missingEndpoints.length === 0) {
99
+ admitted.push(edge);
100
+ continue;
101
+ }
102
+ diagnostics.push(
103
+ ExtractionDiagnostic.create({
104
+ code: "missing-edge-endpoint",
105
+ payload: { edge: edgeKey, missingEndpoints },
106
+ }),
107
+ );
108
+ }
109
+ return admitted;
110
+ }
111
+ }
@@ -0,0 +1,155 @@
1
+ // @unit world-model
2
+ // @layer application
3
+ // @work-item-id WI-295, WI-296
4
+
5
+ import type { CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
6
+ import type { ConstraintFindingDto } from "../../domain/services/constraint-evaluator.js";
7
+ import type { ObligationDerivationService } from "../../domain/services/obligation-derivation-service.js";
8
+ import type {
9
+ PolicyInputsDigestDerivation,
10
+ PolicyInputsDigestDeriver,
11
+ } from "../../domain/services/policy-inputs-digest-deriver.js";
12
+ import type { SnapshotRootDeriver } from "../../domain/services/snapshot-root-deriver.js";
13
+ import type { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
14
+ import type { WorldObligationReportDto } from "../dto/world-obligation-report-dto.js";
15
+ import type { ObligationReportWriterPort } from "../ports/obligation-report-writer-port.js";
16
+ import type {
17
+ AdoptionBaselineRepositoryPort,
18
+ SemanticDebtRepositoryPort,
19
+ WaiverDeclarationRepositoryPort,
20
+ WorldControlDiagnosticDto,
21
+ WorldControlReadResult,
22
+ } from "../ports/world-control-declaration-repository-port.js";
23
+
24
+ export interface DeriveObligationsInput {
25
+ readonly rulesetVersion: string;
26
+ readonly findings: readonly ConstraintFindingDto[];
27
+ readonly corpusRoot: Sha256Digest;
28
+ readonly constraintRoot: Sha256Digest;
29
+ readonly evaluationConfigDigest: Sha256Digest;
30
+ readonly policyAsOfDate: string | null;
31
+ readonly writeReport: boolean;
32
+ readonly reportPath?: string;
33
+ }
34
+
35
+ export type ReportPersistenceResult =
36
+ | { readonly state: "not-requested" }
37
+ | { readonly state: "written" }
38
+ | { readonly state: "failed"; readonly message: string };
39
+
40
+ export type DeriveObligationsResult =
41
+ | {
42
+ readonly status: "derived";
43
+ readonly report: WorldObligationReportDto;
44
+ readonly canonicalBytes: Uint8Array;
45
+ readonly persistence: ReportPersistenceResult;
46
+ }
47
+ | {
48
+ readonly status: "invalid-policy-input";
49
+ readonly diagnostics: readonly WorldControlDiagnosticDto[];
50
+ };
51
+
52
+ interface Dependencies {
53
+ readonly baselineRepository: AdoptionBaselineRepositoryPort;
54
+ readonly waiverRepository: WaiverDeclarationRepositoryPort;
55
+ readonly semanticDebtRepository: SemanticDebtRepositoryPort;
56
+ readonly policyInputsDigestDeriver: PolicyInputsDigestDeriver;
57
+ readonly evaluationIdDeriver: SnapshotRootDeriver;
58
+ readonly obligationDerivationService: ObligationDerivationService;
59
+ readonly serializer: CanonicalJsonSerializer;
60
+ readonly writer: ObligationReportWriterPort;
61
+ }
62
+
63
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
64
+
65
+ const diagnosticsOf = <T>(result: WorldControlReadResult<T>): readonly WorldControlDiagnosticDto[] =>
66
+ result.state === "invalid" ? result.diagnostics : [];
67
+
68
+ export class DeriveObligationsUseCase {
69
+ constructor(private readonly dependencies: Dependencies) {}
70
+
71
+ async execute(input: DeriveObligationsInput): Promise<DeriveObligationsResult> {
72
+ const [baselineResult, waiverResult, debtResult] = await Promise.all([
73
+ this.dependencies.baselineRepository.load(),
74
+ this.dependencies.waiverRepository.load(),
75
+ this.dependencies.semanticDebtRepository.load(),
76
+ ]);
77
+ const diagnostics = [
78
+ ...diagnosticsOf(baselineResult),
79
+ ...diagnosticsOf(waiverResult),
80
+ ...diagnosticsOf(debtResult),
81
+ ].sort(
82
+ (left, right) =>
83
+ compareStrings(left.code, right.code) ||
84
+ compareStrings(left.path, right.path) ||
85
+ compareStrings(left.locator ?? "", right.locator ?? ""),
86
+ );
87
+ if (baselineResult.state === "invalid" || waiverResult.state === "invalid" || debtResult.state === "invalid") {
88
+ return Object.freeze({
89
+ status: "invalid-policy-input",
90
+ diagnostics: Object.freeze(diagnostics),
91
+ });
92
+ }
93
+
94
+ let policy: PolicyInputsDigestDerivation;
95
+ try {
96
+ policy = this.dependencies.policyInputsDigestDeriver.derive({
97
+ baseline: baselineResult.value,
98
+ waivers: waiverResult.value,
99
+ semanticDebts: debtResult.value,
100
+ policyAsOfDate: input.policyAsOfDate,
101
+ });
102
+ } catch (error) {
103
+ return Object.freeze({
104
+ status: "invalid-policy-input",
105
+ diagnostics: Object.freeze([
106
+ {
107
+ code: "invalid-policy-as-of-date",
108
+ path: "world.policyAsOfDate",
109
+ locator: null,
110
+ message: error instanceof Error ? error.message : String(error),
111
+ },
112
+ ]),
113
+ });
114
+ }
115
+ const evaluation = this.dependencies.evaluationIdDeriver.deriveEvaluationId({
116
+ schemaVersion: "phasegate-world-evaluation/v1",
117
+ rulesetVersion: input.rulesetVersion,
118
+ corpusRoot: input.corpusRoot,
119
+ constraintRoot: input.constraintRoot,
120
+ evaluationConfigDigest: input.evaluationConfigDigest,
121
+ policyInputsDigest: policy.digest,
122
+ });
123
+ const report: WorldObligationReportDto = this.dependencies.obligationDerivationService.derive({
124
+ evaluationId: evaluation.evaluationId,
125
+ rulesetVersion: input.rulesetVersion,
126
+ policyInputsDigest: policy.digest,
127
+ findings: input.findings,
128
+ baseline: baselineResult.value,
129
+ waivers: waiverResult.value,
130
+ semanticDebts: debtResult.value,
131
+ policyAsOfDate: policy.resolvedPolicyAsOfDate,
132
+ });
133
+ const canonicalBytes = this.dependencies.serializer.serialize(report);
134
+ let persistence: ReportPersistenceResult = Object.freeze({
135
+ state: "not-requested",
136
+ });
137
+ if (input.writeReport) {
138
+ try {
139
+ await this.dependencies.writer.write(canonicalBytes, input.reportPath);
140
+ persistence = Object.freeze({ state: "written" });
141
+ } catch (error) {
142
+ persistence = Object.freeze({
143
+ state: "failed",
144
+ message: error instanceof Error ? error.message : String(error),
145
+ });
146
+ }
147
+ }
148
+ return Object.freeze({
149
+ status: "derived",
150
+ report,
151
+ canonicalBytes,
152
+ persistence,
153
+ });
154
+ }
155
+ }
@@ -0,0 +1,144 @@
1
+ // @unit world-model
2
+ // @layer application
3
+ // @work-item-id WI-296, WI-297
4
+
5
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
6
+ import type { Snapshot } from "../../domain/entities/snapshot.js";
7
+ import { ConstraintEvaluator, type ConstraintFindingDto } from "../../domain/services/constraint-evaluator.js";
8
+ import type { SnapshotRootDeriver } from "../../domain/services/snapshot-root-deriver.js";
9
+ import { ChangeProvenance } from "../../domain/value-objects/change-provenance.js";
10
+ import { PathKey } from "../../domain/value-objects/path-key.js";
11
+ import type { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
12
+ import type {
13
+ ConstraintDeclarationRepositoryPort,
14
+ WorldControlDiagnosticDto,
15
+ } from "../ports/world-control-declaration-repository-port.js";
16
+ import type { BuildSnapshotContract } from "./build-snapshot-use-case.js";
17
+ import type { DeriveObligationsResult, DeriveObligationsUseCase } from "./derive-obligations-use-case.js";
18
+
19
+ export interface PolicyDatePort {
20
+ currentUtcDate(): string;
21
+ }
22
+
23
+ export type DeriveWorldObligationsResult =
24
+ | { readonly status: "derived"; readonly result: Extract<DeriveObligationsResult, { status: "derived" }> }
25
+ | { readonly status: "execution-failure"; readonly diagnostics: readonly WorldControlDiagnosticDto[] };
26
+
27
+ export interface DeriveWorldObligationsInput {
28
+ readonly writeReport: boolean;
29
+ readonly reportPath?: string;
30
+ readonly baselineSnapshot?: Snapshot;
31
+ }
32
+
33
+ interface Dependencies {
34
+ readonly buildSnapshot: BuildSnapshotContract;
35
+ readonly constraintRepository: ConstraintDeclarationRepositoryPort;
36
+ readonly rootDeriver: SnapshotRootDeriver;
37
+ readonly deriveObligations: DeriveObligationsUseCase;
38
+ readonly policyDate: PolicyDatePort;
39
+ readonly constraintConfigDigest: Sha256Digest;
40
+ readonly evaluationConfigDigest: Sha256Digest;
41
+ }
42
+
43
+ const RULESET_VERSION = "phasegate-world-wcr/v1";
44
+
45
+ export class DeriveWorldObligationsUseCase {
46
+ constructor(private readonly dependencies: Dependencies) {}
47
+
48
+ async execute(input: DeriveWorldObligationsInput): Promise<DeriveWorldObligationsResult> {
49
+ const [snapshot, constraints] = await Promise.all([
50
+ this.dependencies.buildSnapshot.execute(),
51
+ this.dependencies.constraintRepository.load(),
52
+ ]);
53
+ if (constraints.state === "invalid") {
54
+ return { status: "execution-failure", diagnostics: constraints.diagnostics };
55
+ }
56
+ const declarationDiagnostics = [
57
+ ...constraints.value.diagnostics.map((item) =>
58
+ ExtractionDiagnostic.create({
59
+ code: item.code,
60
+ path: PathKey.create(item.path),
61
+ payload: { locator: item.locator, message: item.message },
62
+ }),
63
+ ),
64
+ ...constraints.value.malformedDeclarations.map((declaration) =>
65
+ ExtractionDiagnostic.create({
66
+ code: "malformed-constraint-declaration",
67
+ path: PathKey.create("phasegate.world-constraints.json"),
68
+ payload: declaration.toCanonicalValue(),
69
+ }),
70
+ ),
71
+ ];
72
+ const constraintRoot = this.dependencies.rootDeriver.deriveConstraintRoot({
73
+ schemaVersion: constraints.value.schemaVersion,
74
+ rulesetVersion: RULESET_VERSION,
75
+ constraintConfigDigest: this.dependencies.constraintConfigDigest,
76
+ constraints: constraints.value.records.map((record) => ({
77
+ id: record.constraintId,
78
+ value: record.toCanonicalValue(),
79
+ })),
80
+ explicitClaims: [],
81
+ aliases: constraints.value.aliases.map((alias) => ({
82
+ id: alias.from,
83
+ value: alias.toCanonicalValue(),
84
+ })),
85
+ declarationDiagnostics,
86
+ }).root;
87
+ const evaluation = new ConstraintEvaluator().evaluateFull({
88
+ currentSnapshot: snapshot,
89
+ baselineSnapshot: input.baselineSnapshot,
90
+ records: constraints.value.records,
91
+ malformedDeclarations: constraints.value.malformedDeclarations,
92
+ aliases: constraints.value.aliases,
93
+ relations: constraints.value.relations,
94
+ changeProvenance: ChangeProvenance.between(input.baselineSnapshot ?? null, snapshot),
95
+ });
96
+ const implicitFindings = snapshot.extractionDiagnostics.flatMap((diagnostic): readonly ConstraintFindingDto[] => {
97
+ if (diagnostic.code !== "duplicate-node-id" || diagnostic.nodeId === undefined) return [];
98
+ const facts = Array.isArray(diagnostic.payload.candidateFacts) ? diagnostic.payload.candidateFacts : [];
99
+ const digests = facts
100
+ .flatMap((fact) => {
101
+ if (typeof fact !== "string") return [];
102
+ try {
103
+ const parsed = JSON.parse(fact) as { contentDigest?: unknown };
104
+ return typeof parsed.contentDigest === "string" ? [parsed.contentDigest] : [];
105
+ } catch {
106
+ return [];
107
+ }
108
+ })
109
+ .sort();
110
+ return [
111
+ {
112
+ ruleId: "WCR-005",
113
+ constraintId: null,
114
+ factType: null,
115
+ endpoint: null,
116
+ claimant: null,
117
+ premise: null,
118
+ declarationArtifactId: "",
119
+ declarationLocator: "",
120
+ evidence: {
121
+ nodeId: diagnostic.nodeId.toString(),
122
+ candidateCount:
123
+ typeof diagnostic.payload.candidates === "number" ? diagnostic.payload.candidates : digests.length,
124
+ candidateContentDigests: digests,
125
+ resolution: "global-node-id",
126
+ },
127
+ },
128
+ ];
129
+ });
130
+ const result = await this.dependencies.deriveObligations.execute({
131
+ rulesetVersion: RULESET_VERSION,
132
+ findings: [...evaluation.findings, ...implicitFindings],
133
+ corpusRoot: snapshot.corpusRoot,
134
+ constraintRoot,
135
+ evaluationConfigDigest: this.dependencies.evaluationConfigDigest,
136
+ policyAsOfDate: this.dependencies.policyDate.currentUtcDate(),
137
+ writeReport: input.writeReport,
138
+ reportPath: input.reportPath,
139
+ });
140
+ return result.status === "derived"
141
+ ? { status: "derived", result }
142
+ : { status: "execution-failure", diagnostics: result.diagnostics };
143
+ }
144
+ }
@@ -0,0 +1,74 @@
1
+ // @unit world-model
2
+ // @layer application
3
+ // @work-item-id WI-291
4
+
5
+ import type {
6
+ WorldExtractionDiagnosticDto,
7
+ WorldInspectionDto,
8
+ WorldInventoryCountDto,
9
+ WorldJsonObject,
10
+ } from "../dto/world-inspection-dto.js";
11
+ import type { BuildSnapshotContract } from "./build-snapshot-use-case.js";
12
+
13
+ export interface InspectWorldContract {
14
+ execute(): Promise<WorldInspectionDto>;
15
+ }
16
+
17
+ export interface InspectWorldUseCaseDeps {
18
+ readonly buildSnapshot: BuildSnapshotContract;
19
+ }
20
+
21
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
22
+
23
+ const countValues = (values: readonly string[]): readonly WorldInventoryCountDto[] => {
24
+ const counts = new Map<string, number>();
25
+ for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
26
+ return [...counts.entries()]
27
+ .sort(([left], [right]) => compareStrings(left, right))
28
+ .map(([value, count]) => ({ value, count }));
29
+ };
30
+
31
+ export class InspectWorldUseCase implements InspectWorldContract {
32
+ constructor(private readonly deps: InspectWorldUseCaseDeps) {}
33
+
34
+ async execute(): Promise<WorldInspectionDto> {
35
+ const snapshot = await this.deps.buildSnapshot.execute();
36
+ const nodes = snapshot.nodes
37
+ .map((node) => node.toCanonicalValue() as WorldJsonObject)
38
+ .sort((left, right) => compareStrings(String(left.id), String(right.id)));
39
+ const edges = snapshot.edges
40
+ .map((edge) => edge.toCanonicalValue() as WorldJsonObject)
41
+ .sort((left, right) => compareStrings(JSON.stringify(left), JSON.stringify(right)));
42
+ const diagnostics = snapshot.extractionDiagnostics
43
+ .map((diagnostic) => diagnostic.toCanonicalValue() as WorldExtractionDiagnosticDto)
44
+ .sort((left, right) => compareStrings(JSON.stringify(left), JSON.stringify(right)));
45
+ const artifactNodes = snapshot.nodes.filter((node) => node.projection.type === "artifact");
46
+ const hardDiagnosticCount = snapshot.extractionDiagnostics.filter(
47
+ (diagnostic) => diagnostic.code !== "not-present",
48
+ ).length;
49
+ return {
50
+ snapshotId: snapshot.id.toString(),
51
+ schemaVersion: snapshot.schemaVersion,
52
+ extractorVersion: snapshot.extractorVersion,
53
+ corpusRoot: snapshot.corpusRoot.toString(),
54
+ summary: {
55
+ nodeCount: nodes.length,
56
+ edgeCount: edges.length,
57
+ diagnosticCount: diagnostics.length,
58
+ hardDiagnosticCount,
59
+ },
60
+ inventory: {
61
+ nodeTypes: countValues(snapshot.nodes.map((node) => node.id.nodeType)),
62
+ corpusRoles: countValues(
63
+ artifactNodes.map((node) => (node.projection.type === "artifact" ? node.projection.corpusRole : "")),
64
+ ),
65
+ artifactKinds: countValues(
66
+ artifactNodes.map((node) => (node.projection.type === "artifact" ? node.projection.artifactKind : "")),
67
+ ),
68
+ },
69
+ nodes,
70
+ edges,
71
+ diagnostics,
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,139 @@
1
+ // @unit world-model
2
+ // @layer application
3
+ // @work-item-id WI-296
4
+
5
+ import { ConstraintRecord } from "../../domain/entities/constraint-record.js";
6
+ import type { Snapshot } from "../../domain/entities/snapshot.js";
7
+ import { NodePin } from "../../domain/value-objects/node-pin.js";
8
+ import { WorldNodeId } from "../../domain/value-objects/world-node-id.js";
9
+ import type {
10
+ ConstraintDeclarationRepositoryPort,
11
+ ConstraintDeclarationSet,
12
+ WorldControlDiagnosticDto,
13
+ } from "../ports/world-control-declaration-repository-port.js";
14
+ import type { BuildSnapshotContract } from "./build-snapshot-use-case.js";
15
+
16
+ export type PinEndpointRole = "claimant" | "premise";
17
+
18
+ export interface PinConstraintEndpointInput {
19
+ readonly constraintId: string;
20
+ readonly endpoint: PinEndpointRole;
21
+ readonly apply: boolean;
22
+ }
23
+
24
+ export type PinConstraintEndpointResult =
25
+ | { readonly status: "preview" | "applied" | "unchanged"; readonly candidate: PinCandidateDto }
26
+ | { readonly status: "domain-failure"; readonly code: string; readonly message: string }
27
+ | { readonly status: "execution-failure"; readonly diagnostics: readonly WorldControlDiagnosticDto[] };
28
+
29
+ export interface PinCandidateDto {
30
+ readonly constraintId: string;
31
+ readonly endpoint: PinEndpointRole;
32
+ readonly nodeId: string;
33
+ readonly beforeDigest: string;
34
+ readonly afterDigest: string;
35
+ readonly changed: boolean;
36
+ }
37
+
38
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
39
+
40
+ const serialize = (set: ConstraintDeclarationSet, replacement: ConstraintRecord): unknown => ({
41
+ schemaVersion: set.schemaVersion,
42
+ constraints: set.records
43
+ .map((record) => (record.constraintId.equals(replacement.constraintId) ? replacement : record))
44
+ .sort((left, right) => compareStrings(left.constraintId.toString(), right.constraintId.toString()))
45
+ .map((record) => ({
46
+ constraintId: record.constraintId.toString(),
47
+ factType: record.factType,
48
+ claimant: record.claimant.toCanonicalValue(),
49
+ premise: record.premise.toCanonicalValue(),
50
+ applicableRuleIds: record.applicableRuleIds.map((ruleId) => ruleId.toString()),
51
+ })),
52
+ aliases: set.aliases
53
+ .map((alias) => ({ from: alias.from.toString(), to: alias.to.toString() }))
54
+ .sort((left, right) => compareStrings(left.from, right.from)),
55
+ });
56
+
57
+ const resolveNode = (snapshot: Snapshot, set: ConstraintDeclarationSet, nodeId: WorldNodeId) => {
58
+ const duplicate = snapshot.extractionDiagnostics.some(
59
+ (diagnostic) => diagnostic.code === "duplicate-node-id" && diagnostic.nodeId?.equals(nodeId),
60
+ );
61
+ if (duplicate) return { ok: false as const, code: "duplicate-endpoint", message: `duplicate endpoint: ${nodeId}` };
62
+ const exact = snapshot.nodes.filter((node) => node.id.equals(nodeId));
63
+ if (exact.length === 1) return { ok: true as const, node: exact[0] };
64
+ const aliases = set.aliases.filter((alias) => alias.from.equals(nodeId));
65
+ if (aliases.length !== 1 || set.aliases.some((alias) => alias.from.equals(aliases[0]?.to))) {
66
+ return {
67
+ ok: false as const,
68
+ code: aliases.length === 0 ? "missing-endpoint" : "ambiguous-endpoint-alias",
69
+ message: `endpoint cannot be resolved uniquely: ${nodeId}`,
70
+ };
71
+ }
72
+ const targets = snapshot.nodes.filter((node) => node.id.equals(aliases[0].to));
73
+ return targets.length === 1
74
+ ? { ok: true as const, node: targets[0] }
75
+ : {
76
+ ok: false as const,
77
+ code: targets.length === 0 ? "missing-endpoint" : "duplicate-endpoint",
78
+ message: `alias target cannot be resolved uniquely: ${aliases[0].to}`,
79
+ };
80
+ };
81
+
82
+ export class PinConstraintEndpointUseCase {
83
+ constructor(
84
+ private readonly buildSnapshot: BuildSnapshotContract,
85
+ private readonly repository: ConstraintDeclarationRepositoryPort,
86
+ ) {}
87
+
88
+ async execute(input: PinConstraintEndpointInput): Promise<PinConstraintEndpointResult> {
89
+ let constraintId: WorldNodeId;
90
+ try {
91
+ constraintId = WorldNodeId.parse(input.constraintId);
92
+ if (constraintId.nodeType !== "constraint") throw new Error("not constraint ID");
93
+ } catch {
94
+ return { status: "domain-failure", code: "invalid-constraint-id", message: input.constraintId };
95
+ }
96
+ const [snapshot, loaded] = await Promise.all([this.buildSnapshot.execute(), this.repository.load()]);
97
+ if (loaded.state === "invalid") return { status: "execution-failure", diagnostics: loaded.diagnostics };
98
+ if (loaded.value.malformedDeclarations.length > 0 || loaded.value.diagnostics.length > 0) {
99
+ return {
100
+ status: "domain-failure",
101
+ code: "malformed-constraint-declaration",
102
+ message: "constraint declarations contain malformed or ambiguous candidates",
103
+ };
104
+ }
105
+ const records = loaded.value.records.filter((record) => record.constraintId.equals(constraintId));
106
+ if (records.length !== 1) {
107
+ return { status: "domain-failure", code: "constraint-not-found", message: input.constraintId };
108
+ }
109
+ const record = records[0];
110
+ const before = input.endpoint === "claimant" ? record.claimant : record.premise;
111
+ const resolved = resolveNode(snapshot, loaded.value, before.nodeId);
112
+ if (!resolved.ok) return { status: "domain-failure", code: resolved.code, message: resolved.message };
113
+ const after = NodePin.create({ nodeId: before.nodeId, contentDigest: resolved.node.contentDigest });
114
+ const replacement = ConstraintRecord.create({
115
+ constraintId: record.constraintId,
116
+ schemaVersion: record.schemaVersion,
117
+ factType: record.factType,
118
+ claimant: input.endpoint === "claimant" ? after : record.claimant,
119
+ premise: input.endpoint === "premise" ? after : record.premise,
120
+ applicableRuleIds: record.applicableRuleIds,
121
+ declarationArtifactId: record.declarationArtifactId,
122
+ declarationLocator: record.declarationLocator,
123
+ });
124
+ const candidate = Object.freeze({
125
+ constraintId: input.constraintId,
126
+ endpoint: input.endpoint,
127
+ nodeId: before.nodeId.toString(),
128
+ beforeDigest: before.contentDigest.toString(),
129
+ afterDigest: after.contentDigest.toString(),
130
+ changed: !before.contentDigest.equals(after.contentDigest),
131
+ });
132
+ if (!input.apply) return { status: "preview", candidate };
133
+ if (!candidate.changed) return { status: "unchanged", candidate };
134
+ const written = await this.repository.replaceAtomically(serialize(loaded.value, replacement));
135
+ return written.state === "written"
136
+ ? { status: "applied", candidate }
137
+ : { status: "execution-failure", diagnostics: written.diagnostics };
138
+ }
139
+ }