phasegate 0.167.0 → 0.172.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 (54) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/docs/ADR/021-severity-contract.md +46 -0
  3. package/package.json +1 -1
  4. package/scripts/harness/attestation/application/dto/attestation-document.ts +2 -0
  5. package/scripts/harness/attestation/application/dto/verify-attestation-output.ts +2 -0
  6. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +13 -0
  7. package/scripts/harness/attestation/application/ports/ac-bound-allowlist-port.ts +12 -0
  8. package/scripts/harness/attestation/application/ports/matrix-source-port.ts +16 -0
  9. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +39 -0
  10. package/scripts/harness/attestation/application/usecases/verify-attestation-usecase.ts +60 -1
  11. package/scripts/harness/attestation/composition-root.ts +14 -0
  12. package/scripts/harness/attestation/domain/entities/attestation-record.ts +13 -0
  13. package/scripts/harness/attestation/domain/services/ac-bound-scope-service.ts +80 -0
  14. package/scripts/harness/attestation/infrastructure/adapters/config-ac-bound-allowlist-adapter.ts +23 -0
  15. package/scripts/harness/attestation/infrastructure/adapters/file-system-matrix-source-adapter.ts +25 -0
  16. package/scripts/harness/attestation/presentation/handlers/verify-attestation-handler.ts +1 -0
  17. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +3 -0
  18. package/scripts/harness/config-foundation/domain/harness-config.ts +1 -0
  19. package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
  20. package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
  21. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +7 -0
  22. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +7 -0
  23. package/scripts/harness/harness-error/domain/errors/severity-downgrade-violation-error.ts +1 -1
  24. package/scripts/harness/nyquist-validation/application/mappers/coverage-result-mapper.ts +13 -15
  25. package/scripts/harness/nyquist-validation/application/mappers/impact-analysis-result-mapper.ts +12 -12
  26. package/scripts/harness/nyquist-validation/application/usecases/analyze-impact-usecase.ts +11 -11
  27. package/scripts/harness/nyquist-validation/application/usecases/calculate-coverage-usecase.ts +11 -11
  28. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +9 -9
  29. package/scripts/harness/nyquist-validation/domain/aggregates/requirement-test-matrix.ts +1 -1
  30. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +1 -1
  31. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +1 -1
  32. package/scripts/harness/nyquist-validation/domain/services/coverage-calculation-service.ts +1 -1
  33. package/scripts/harness/nyquist-validation/domain/services/impact-analysis-service.ts +2 -2
  34. package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +5 -5
  35. package/scripts/harness/nyquist-validation/domain/value-objects/impact-analysis-result.ts +1 -1
  36. package/scripts/harness/nyquist-validation/infrastructure/adapters/ajv-json-schema-validator-adapter.ts +3 -3
  37. package/scripts/harness/nyquist-validation/infrastructure/adapters/config-foundation-coverage-threshold-adapter.ts +3 -3
  38. package/scripts/harness/validator-system/application/dto/run-l3-validators-input.ts +2 -0
  39. package/scripts/harness/validator-system/application/use-cases/run-l1-validators-usecase.ts +1 -1
  40. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +3 -3
  41. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +33 -3
  42. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +3 -3
  43. package/scripts/harness/validator-system/application/use-cases/run-quick-mode-usecase.ts +3 -3
  44. package/scripts/harness/validator-system/composition-root.ts +12 -0
  45. package/scripts/harness/validator-system/domain/ports/ac-bound-coverage-policy-port.ts +22 -0
  46. package/scripts/harness/validator-system/domain/services/validator-execution-service.ts +1 -1
  47. package/scripts/harness/validator-system/domain/services/validator-registry.ts +2 -2
  48. package/scripts/harness/validator-system/domain/value-objects/layer-config.ts +1 -1
  49. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +1 -1
  50. package/scripts/harness/validator-system/domain/value-objects/validator-definition.ts +2 -2
  51. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +2 -0
  52. package/scripts/harness/validator-system/infrastructure/adapters/json-coverage-report-adapter.ts +1 -1
  53. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-bound-coverage-policy-adapter.ts +118 -0
  54. package/scripts/harness/validator-system/presentation/handlers/report-validation-results-handler.ts +4 -4
package/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.171.0] - 2026-07-05
11
+
12
+ ### Added
13
+
14
+ - **WI-227 / H16-03 — L3-005 "AC-bound coverage" gate (fail-closed, default-OFF)** — new opt-in L3 validator that, for each story in `layers.L3.acBoundStories`, fail-closed verifies that **every** linked AC has ≥1 `binding:"ac"` test reference (`fileFallbackOnly===0`). Unlike L3-004 (story-level) this is per-AC binding. `ac-bound-coverage-policy-port.ts` (domain) + `nyquist-ac-bound-coverage-policy-adapter.ts` (infrastructure, reads the matrix and fail-closes on absent/parse-fail); `ValidatorId` gains `L3-005 → ac-bound-coverage`; `RunL3ValidatorsUseCase` gains an L3-005 override block mirroring L3-004 (only when config enables L3-005). **NOT** added to `DEFAULT_CONFIG` nor to standard/strict presets (default-OFF opt-in). config alias `ac-bound-coverage → L3-005`; new `layers.L3.acBoundStories?: string[]` (additive-safe; existing configs still load).
15
+ - **WI-227 / H16-03 — attestation `acBoundScope`** — attestation records now carry a machine-readable `acBoundScope: string[]` (sorted; stories that are genuinely ac-bound AND in scope AND passing). New pure domain `AcBoundScopeService.derive(matrix, allowlist)`; new application ports `MatrixSourcePort` / `AcBoundAllowlistPort` with `FileSystemMatrixSourceAdapter` / `ConfigAcBoundAllowlistAdapter`. `acBoundScope` is included in the canonical payload (covered by `attestationDigest`) and in `equals()`/`create/reconstruct/seal`. **verify RE-DERIVES** `acBoundScope` from the stored (hash-verified) matrix + config allowlist and compares to stored (anti-laundering); unreadable/unparsable re-derivation inputs → fail-closed (exit 1). `GranularityDerivationService` is UNTOUCHED — `granularity.traceability.level` stays `"file"` (acBoundScope is an independent per-story dimension, no global flip).
16
+ - **Attestation readiness docs (debt repayment)** — authored the three readiness docs that had been missing for the attestation unit since 手1/v0.165.0: `docs/product/construction/attestation/unit_test_design.md`, `it_test_design.md`, `coverage_report.md`. Coverage is measured (vitest v8: 83.53% statements/lines, 78.08% branches, 97.75% functions on the pre-slice baseline) with the exact reproduction command recorded; numbers are real, not fabricated.
17
+ - **`l3-005-ac-bound-ratchet.md`** — new honesty/ratchet doc mirroring the l3-004 R-table; first scope `["HF2-05"]`, records H05-02 excluded (legacy) and H06-03 partial (only AC-4).
18
+
19
+ ### Changed
20
+
21
+ - **Self-repo `phasegate.config.json` only** — `layers.L3.validators` gains `L3-005` and `layers.L3.acBoundStories: ["HF2-05"]`. Product presets (`standard.json`/`strict.json`) and `DEFAULT_CONFIG` are untouched — no impact on other users. HF2-05 is genuinely ac-bound (all 6 ACs have ≥1 `binding:"ac"`), so enabling L3-005 keeps self-repo CI green.
22
+ - Design docs updated design-first: validator-system `logical_design.md` (§1.3, §2.1.1 L3-005 row, §3.4.2 note, §4.8b adapter), attestation `logical_design.md` (§1.3, §1.4.1 acBoundScope-in-canonical note, §1.4.3 record format, §4.4/§4.5/§4.6) and `domain_model.md` (INV-8), `user_stories.md` (H16-03 + summary tables), config schemas v2/v3 (`acBoundStories`).
23
+
24
+ ## [0.170.0] - 2026-07-05
25
+
26
+ ### Added
27
+
28
+ - **WI-226 / H05-02 — real-corpus ADR integration test (honest-partial)** — closes the honesty gap recorded in `l3-004-traceability-ratchet.md` §6b ("no test verifies the real `docs/ADR/` corpus exists / conforms / has valid status"). New `scripts/harness/__tests__/integration/adr-foundation/real-adr-corpus.it.test.ts` wires the **real** `createAdrFoundationModule(<repoRoot>/docs/ADR)` pipeline against the **committed** corpus (no `mkdtemp`, repoRoot resolved from the test file location) and **fail-closed verifies** four SCOPED properties: (1) canonical (`NNN-slug.md`) ADRs are discovered with 019/020/021 present (membership + floor, property-based so ADR-022 won't break it); (2) every discovered ADR conforms via `validate-all` (zero violations); (3) each discovered ADR's status ∈ {Accepted, Proposed}; (4) the **≥18 legacy `ADR-NNN-*.md` files exist on disk but are provably excluded from discovery** — pinning the known gate-invisibility limitation as a tested invariant.
29
+ - **Honest-partial framing — H05-02 NOT over-claimed** — the new file carries **only** a file-level `// @story H05-02` tag and **deliberately no `// @ac H05-02-N` tags**. The §12 Key Decisions that H05-02 AC-1/2/3 enumerate live mostly in the legacy (gate-invisible) corpus, so the test genuinely verifies only the discoverable canonical corpus + the legacy-exclusion invariant — it does not fully assert AC-1/2/3 as written. Adding `@ac` tags would flip those ACs to `binding:"ac"` and over-claim per-AC verification of an unverified corpus. **H05-02 AC-1/2/3 remain `fileFallbackOnly`** (confirmed via `phasegate:generate-matrix`: all H05-02 references stay `binding:"file"`). Per-AC binding is deferred until the legacy corpus is normalized to canonical format (adr-gate-normalization-followup.md); H05-02 stays out of any future L3-005 "AC-bound coverage" scope until then. `l3-004-traceability-ratchet.md` §6b updated to record the now-existing real-corpus test and the retained fileFallbackOnly status. No gate behavior, matrix byte-equality, or attestation granularity (`level:"file"`) changed.
30
+
31
+ ## [0.169.0] - 2026-07-05
32
+
33
+ ### Fixed
34
+
35
+ - **WI-225 / H06-03 AC-4 — severity contract violation message now carries its ADR basis (correct ADR-021)** — `SeverityDowngradeViolationError` previously threw a message stating only the violation content (`default=…` / `requested=…`) but **omitted the ADR reference** that H06-03 AC-4 requires ("契約違反時のエラーメッセージに違反内容と根拠(ADR参照)が含まれる"). The message now appends `根拠: ADR-021`. A new **ADR-021 "severity 契約 — 格下げ禁止"** (`docs/ADR/021-severity-contract.md`, status Accepted) forward-documents the existing downgrade-prohibition rule (implemented in `SeverityContractEnforcer`, specified in `harness-error/logical_design.md` §2.3.3 / §4.6) and is the ADR that actually decides the prohibition. This corrects a prior draft that referenced **ADR-017 by mistake** — ADR-017 governs warning-severity aggregation (`failOnWarning`), **not** the downgrade prohibition; citing it was a plausible-but-wrong reference of exactly the kind phasegate exists to prevent. The domain unit test (`severity-contract-enforcer.test.ts`) asserts the message contains the violation content **and** `ADR-021`. Constructor signature is unchanged (single call site is the enforcer); no API contract change.
36
+
37
+ ### Changed
38
+
39
+ - **Honest `@ac` backfill — 3 ACs promoted to `binding:"ac"` (no new gate, no attestation change)** — prerequisite work for a future fail-closed L3-005 gate. Global `acLevelCoverage.acBound` moves **4 → 7**:
40
+ - **H06-03 AC-4** — tagged (`// @ac AC-4`) on the new severity-contract-enforcer assertion above.
41
+ - **HF2-05 AC-1 / AC-2** — tagged on the tracer's own genuine assertions in `type-script-test-reference-source-adapter-ac.it.test.ts` using the **absolute** form (`// @ac HF2-05-1` / `// @ac HF2-05-2`). Assessed as SAFE: the file's `storyCount` is 9 (real `@story HF2-05` plus fixture `@story` string-literals), so the **relative** form (`AC-N`) would become a `relative-multi-story` orphan; the absolute form binds against the file's real storyId and was empirically confirmed to add exactly 2 correct `binding:"ac"` references with **zero** new spurious/orphan matrix entries (total refs and orphan-bearing refs unchanged). **HF2-05 is now `fileFallbackOnly === 0`** (all 6 ACs `binding:"ac"`).
42
+ - **H05-02 left untouched** (AC-1/2/3 remain `fileFallbackOnly`); its genuine test is planned separately.
43
+ - No `binding` was faked; L3-004 verdict and attestation granularity (`level:"file"`) are unchanged.
44
+
45
+ ## [0.168.0] - 2026-07-05
46
+
47
+ ### Fixed
48
+
49
+ - **WI-223 — biome dogfooding cleanup (`nyquist-validation` / `validator-system` units)** — cleared pre-existing repo-wide biome lint debt in files not touched by recent work. All fixes are mechanical and behavior-preserving:
50
+ - `lint/complexity/useLiteralKeys` (58 diagnostics) — converted string-literal computed member access (`obj['stories']`) to dot access (`obj.stories`) where the key is a valid identifier, across the coverage/impact use-cases, `matrix-validation-service`, the AJV adapter, the coverage-threshold adapter, the JSON coverage-report adapter, and the validation-result contract handler.
51
+ - `lint/style/useImportType` (27 diagnostics) — marked type-only imports with `import type` (or `type` specifiers) across `nyquist-validation` domain entities/aggregates/services/value-objects and `validator-system` L1–L4 use-cases and domain services/value-objects.
52
+ - `lint/complexity/noStaticOnlyClass` (2 diagnostics) — converted the static-only `CoverageResultMapper` and `ImpactAnalysisResultMapper` classes into plain exported functions (`toCalculateCoverageOutput`, `toAnalyzeImpactOutput`) and updated their single call sites in `calculate-coverage-usecase` / `analyze-impact-usecase`. No public behavior change.
53
+
10
54
  ## [0.167.0] - 2026-07-05
11
55
 
12
56
  ### Added
@@ -0,0 +1,46 @@
1
+ ---
2
+ adr_id: "021"
3
+ title: "severity 契約 — 格下げ禁止"
4
+ status: Accepted
5
+ date: 2026-07-05
6
+ ---
7
+
8
+ # severity 契約 — 格下げ禁止
9
+
10
+ ## Context
11
+
12
+ phasegate は品質防御ツールであり、その検査結果 severity(`error` / `warning`)はゲートの通過可否を左右する。各 error code は canonical カタログ(`ErrorDefinition`)に `defaultSeverity` を持ち、これがその code の最小限の重大度を規定する。
13
+
14
+ 呼び出し側(orchestration 層 / CLI)は effective severity を明示的に要求できるが、ここに **severity を静かに弱めてゲートをすり抜ける** という抜け道のリスクがある。例えば `defaultSeverity=error` の違反を `warning` に格下げすれば、`--fail-on-error` 系の終了コード判定を回避できてしまう。これは「チェッカーは常に機械であり、trust は機械的・再現可能な検査のみが付与する」という ADR-019 の trust 境界、および「単方向の強制は交渉不可」という ADR-020 の単方向規律の精神に反する — severity の格下げは、検査が既に検出した違反を事後的に無力化する行為だからである。
15
+
16
+ この格下げ禁止ルールの実体は既に `scripts/harness/harness-error/domain/services/severity-contract-enforcer.ts`(`SeverityContractEnforcer`)に実装され、`docs/product/construction/harness-error/logical_design.md` §2.3.3 / §4.6 に規定されている。しかし ADR としては未文書化であり、`SeverityDowngradeViolationError` のメッセージは根拠として ADR-017(warning severity の集計 / `failOnWarning`)を誤って参照していた。ADR-017 は集計側の決定であって格下げ禁止を定めるものではない。本 ADR-021 は、この既存ルールを前方文書化し、格下げ禁止を実際に決定する ADR として位置づける。
17
+
18
+ ## Decision
19
+
20
+ error の effective severity を、その canonical `defaultSeverity` より **下位へ格下げすることを禁止する。**
21
+
22
+ - `SeverityContractEnforcer.resolveEffectiveSeverity(requested, defaultSeverity)` は次のように振る舞う。
23
+ 1. `requested` 未指定なら `defaultSeverity` を返す。
24
+ 2. `requested` が `defaultSeverity` より高位(格上げ)なら `requested` を返す — 格上げは許容する。
25
+ 3. `requested` が `defaultSeverity` と同一なら `requested` を返す。
26
+ 4. それ以外(格下げ)は `SeverityDowngradeViolationError` を throw する。
27
+ - 不変条件として `error -> warning` の格下げは常に拒否される。
28
+ - 格下げ違反時のメッセージは **違反内容(default / requested の両 severity)と根拠(本 ADR-021)** を必ず含む。これにより違反の再現・監査が機械的に可能になる。
29
+
30
+ trust 境界の性質上(ADR-019)、この判定は LLM を一切用いず、値の比較のみで機械的に完結する。
31
+
32
+ ## Consequences
33
+
34
+ - severity が黙って弱められてゲートをすり抜けることが構造的に不可能になる。attestation の「green」が severity の観点でも信頼できる。
35
+ - 呼び出し側は severity を格上げ(より厳格化)することはできるが、格下げはできない。厳格化の一方向のみが許容され、ADR-020 の単方向規律と整合する。
36
+ - `defaultSeverity` を運用上どうしても下げたい正当なケースがある場合、それは呼び出し側での格下げではなく **canonical カタログ(`ErrorDefinition`)の `defaultSeverity` そのものを前方フローで変更する** ことでしか実現できない。これは意図的な設計判断を前方ゲート経由で通すことを強制する。
37
+
38
+ ## Alternatives
39
+
40
+ 1. **格下げを警告のみとし throw しない** — 事実上の格下げを許すため抜け道が残る。品質防御の目的に反するため不採用。
41
+
42
+ 2. **格下げ禁止を throw で強制(本決定で採用)** — 格下げは `SeverityDowngradeViolationError` を throw し、メッセージに違反内容と根拠 ADR-021 を含める。
43
+
44
+ 3. **呼び出し側で任意の severity 上書きを許容し、カタログを advisory 扱いにする** — canonical カタログの規範性が失われ、severity 契約が有名無実化するため不採用。
45
+
46
+ 関連: ADR-019(「AI 非依存」の境界 — trust 境界は機械的)、ADR-020(単方向規律と逆流学習)、ADR-006(エージェント非依存設計原則)、ADR-017(warning severity の集計 / `failOnWarning` — 集計側の別決定)、`docs/product/construction/harness-error/logical_design.md` §2.3.3 / §4.6。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.167.0",
3
+ "version": "0.172.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -39,6 +39,8 @@ export interface AttestationDocument {
39
39
  readonly granularity: {
40
40
  readonly traceability: AttestationDocumentGranularityClaim;
41
41
  };
42
+ /** H16-03: 実際に ac-bound かつ L3-005 スコープ内で pass した story-id(昇順)。 */
43
+ readonly acBoundScope: string[];
42
44
  readonly metadata: {
43
45
  readonly producedAt: string;
44
46
  readonly producer: string;
@@ -15,6 +15,8 @@ export interface VerifyAttestationChecks {
15
15
  readonly inputHashes: boolean;
16
16
  /** granularity を validatorSet から再導出 == 格納値(anti-laundering) */
17
17
  readonly granularity: boolean;
18
+ /** acBoundScope を stored matrix + allowlist から再導出 == 格納値(anti-laundering, H16-03) */
19
+ readonly acBoundScope: boolean;
18
20
  }
19
21
 
20
22
  /**
@@ -75,6 +75,7 @@ export class AttestationRecordMapper {
75
75
  knownLimitations: [...record.granularity.traceability.knownLimitations],
76
76
  },
77
77
  },
78
+ acBoundScope: [...record.acBoundScope],
78
79
  metadata: {
79
80
  producedAt: record.metadata.producedAt,
80
81
  producer: record.metadata.producer,
@@ -153,6 +154,8 @@ export class AttestationRecordMapper {
153
154
  }
154
155
  const traceability = this.toGranularityClaim(granularity.traceability);
155
156
 
157
+ const acBoundScope = this.toAcBoundScope(doc.acBoundScope);
158
+
156
159
  const metadata = doc.metadata;
157
160
  if (!isObject(metadata)) {
158
161
  throw new MalformedAttestationError("metadata must be an object");
@@ -181,6 +184,7 @@ export class AttestationRecordMapper {
181
184
  gitCommit,
182
185
  },
183
186
  signature: this.toSignatureBlock(mode, attestationDigest, signature),
187
+ acBoundScope,
184
188
  };
185
189
 
186
190
  // verify 用の再構築: INV-1/INV-3 は verify usecase が機械的チェックとして検出するため
@@ -188,6 +192,15 @@ export class AttestationRecordMapper {
188
192
  return AttestationRecord.reconstruct(props);
189
193
  }
190
194
 
195
+ private toAcBoundScope(v: unknown): string[] {
196
+ // H16-03: 後方互換のため未設定は [] とみなす(additive-safe)。
197
+ if (v === undefined || v === null) return [];
198
+ if (!Array.isArray(v)) {
199
+ throw new MalformedAttestationError("acBoundScope must be an array of strings");
200
+ }
201
+ return v.map((item, i) => requireString(item, `acBoundScope[${i}]`));
202
+ }
203
+
191
204
  private toDigest(v: unknown, field: string): Digest {
192
205
  const raw = requireString(v, field);
193
206
  try {
@@ -0,0 +1,12 @@
1
+ // @unit attestation
2
+ // @layer application
3
+
4
+ /**
5
+ * AcBoundAllowlistPort(H16-03 / WI-227)
6
+ *
7
+ * acBoundScope 導出のスコープ(`layers.L3.acBoundStories`)を供給する application ポート。
8
+ */
9
+ export interface AcBoundAllowlistPort {
10
+ /** スコープ対象 story-id を返す(未設定は [])。 */
11
+ getAcBoundStories(): Promise<readonly string[]>;
12
+ }
@@ -0,0 +1,16 @@
1
+ // @unit attestation
2
+ // @layer application
3
+
4
+ /**
5
+ * MatrixSourcePort(H16-03 / WI-227)
6
+ *
7
+ * acBoundScope 導出用の requirement-test-matrix を供給する application ポート。
8
+ * 読み込み/parse に失敗した場合は throw する(呼び出し側 usecase が fail-closed に変換する)。
9
+ */
10
+ export interface MatrixSourcePort {
11
+ /**
12
+ * @param matrixFilePath matrix のパス(省略時は adapter の既定パス)
13
+ * @returns parse 済みの matrix plain object
14
+ */
15
+ load(matrixFilePath?: string): Promise<unknown>;
16
+ }
@@ -4,6 +4,7 @@
4
4
  import { AttestationRecord, type GateResult, type SourceEntry } from "../../domain/entities/attestation-record.js";
5
5
  import type { ContentHasherPort } from "../../domain/ports/content-hasher-port.js";
6
6
  import type { GranularityDerivationService } from "../../domain/services/granularity-derivation-service.js";
7
+ import type { AcBoundScopeService } from "../../domain/services/ac-bound-scope-service.js";
7
8
  import { SignatureBlock } from "../../domain/value-objects/signature-block.js";
8
9
  import { ValidatorOutcome } from "../../domain/value-objects/validator-outcome.js";
9
10
  import type { AttestationDocument } from "../dto/attestation-document.js";
@@ -12,6 +13,8 @@ import type { AttestationRecordMapper } from "../mappers/attestation-record-mapp
12
13
  import type { AttestationRepositoryPort } from "../ports/attestation-repository-port.js";
13
14
  import type { GateResultSourcePort } from "../ports/gate-result-source-port.js";
14
15
  import type { SourceDigesterPort } from "../ports/source-digester-port.js";
16
+ import type { MatrixSourcePort } from "../ports/matrix-source-port.js";
17
+ import type { AcBoundAllowlistPort } from "../ports/ac-bound-allowlist-port.js";
15
18
 
16
19
  const SCHEMA_VERSION = "phasegate-attestation/v1";
17
20
  const PREDICATE_TYPE = "https://phasegate.dev/attestation/gate-run/v1";
@@ -35,6 +38,14 @@ export interface ProduceAttestationDeps {
35
38
  readonly clock?: () => Date;
36
39
  /** 入力 source パス群(既定は config + matrix)。 */
37
40
  readonly inputSourcePaths?: readonly string[];
41
+ /** H16-03: acBoundScope 導出用 matrix 供給(省略時は acBoundScope=[])。 */
42
+ readonly matrixSource?: MatrixSourcePort;
43
+ /** H16-03: acBoundScope 導出用 allowlist 供給(省略時は acBoundScope=[])。 */
44
+ readonly allowlist?: AcBoundAllowlistPort;
45
+ /** H16-03: acBoundScope 導出サービス(省略時は acBoundScope=[])。 */
46
+ readonly acBoundScopeService?: AcBoundScopeService;
47
+ /** H16-03: matrix 供給元パス(inputSourcePaths のうち matrix にあたるもの。既定は 2 番目の source)。 */
48
+ readonly matrixFilePath?: string;
38
49
  }
39
50
 
40
51
  export interface ProduceAttestationResult {
@@ -76,10 +87,15 @@ export class ProduceAttestationUseCase {
76
87
  producedAt: "1970-01-01T00:00:00Z",
77
88
  producer: `phasegate-attestation/${this.deps.pkgVersion}`,
78
89
  gitCommit: null,
90
+ acBoundScope: [],
79
91
  });
80
92
  const inputDigest = preInputRecord.computeInputDigest(this.deps.hasher);
81
93
 
82
94
  // 5. granularity 導出(domain service)
95
+ // 5b. acBoundScope 導出(H16-03): matrixSource + allowlist + AcBoundScopeService。
96
+ // いずれか未配線なら acBoundScope=[]。
97
+ const acBoundScope = await this.deriveAcBoundScope();
98
+
83
99
  // 6. metadata 構築
84
100
  const now = (this.deps.clock ?? (() => new Date()))().toISOString();
85
101
  const gitCommit = await this.deps.gitCommitProvider();
@@ -93,6 +109,7 @@ export class ProduceAttestationUseCase {
93
109
  producedAt: now,
94
110
  producer: `phasegate-attestation/${this.deps.pkgVersion}`,
95
111
  gitCommit,
112
+ acBoundScope,
96
113
  });
97
114
  const sealed = record.seal(this.deps.hasher);
98
115
 
@@ -112,6 +129,7 @@ export class ProduceAttestationUseCase {
112
129
  producedAt: string;
113
130
  producer: string;
114
131
  gitCommit: string | null;
132
+ acBoundScope: readonly string[];
115
133
  }): AttestationRecord {
116
134
  const granularity = this.deps.granularityService.derive(args.validatorSet);
117
135
  return AttestationRecord.create({
@@ -135,9 +153,30 @@ export class ProduceAttestationUseCase {
135
153
  },
136
154
  // 仮の unsigned-poc block(seal 前)。seal が正しい digest で置換する。
137
155
  signature: SignatureBlock.unsignedPoc(args.inputDigest),
156
+ acBoundScope: args.acBoundScope,
138
157
  });
139
158
  }
140
159
 
160
+ /**
161
+ * H16-03: matrixSource + allowlist + AcBoundScopeService から acBoundScope を導出する。
162
+ * いずれかのポート/サービスが未配線なら [] を返す(additive-safe)。
163
+ * matrix の読み込みに失敗した場合も [] を返す(produce は attest の記録が主目的であり、
164
+ * 過大主張の防止は verify の再導出比較が担保する)。
165
+ */
166
+ private async deriveAcBoundScope(): Promise<string[]> {
167
+ if (!this.deps.matrixSource || !this.deps.allowlist || !this.deps.acBoundScopeService) {
168
+ return [];
169
+ }
170
+ try {
171
+ const allowlist = await this.deps.allowlist.getAcBoundStories();
172
+ if (allowlist.length === 0) return [];
173
+ const matrix = await this.deps.matrixSource.load(this.deps.matrixFilePath);
174
+ return this.deps.acBoundScopeService.derive(matrix, allowlist);
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+
141
180
  private async buildSources(): Promise<SourceEntry[]> {
142
181
  const paths = this.deps.inputSourcePaths ?? DEFAULT_INPUT_SOURCES;
143
182
  const sources: SourceEntry[] = [];
@@ -4,11 +4,14 @@
4
4
  import type { AttestationRecord } from "../../domain/entities/attestation-record.js";
5
5
  import type { ContentHasherPort } from "../../domain/ports/content-hasher-port.js";
6
6
  import type { GranularityDerivationService } from "../../domain/services/granularity-derivation-service.js";
7
+ import type { AcBoundScopeService } from "../../domain/services/ac-bound-scope-service.js";
7
8
  import type { VerifyAttestationInput } from "../dto/verify-attestation-input.js";
8
9
  import type { VerifyAttestationChecks, VerifyAttestationOutput } from "../dto/verify-attestation-output.js";
9
10
  import type { AttestationRecordMapper } from "../mappers/attestation-record-mapper.js";
10
11
  import type { AttestationRepositoryPort } from "../ports/attestation-repository-port.js";
11
12
  import type { SourceDigesterPort } from "../ports/source-digester-port.js";
13
+ import type { MatrixSourcePort } from "../ports/matrix-source-port.js";
14
+ import type { AcBoundAllowlistPort } from "../ports/ac-bound-allowlist-port.js";
12
15
 
13
16
  export interface VerifyAttestationDeps {
14
17
  readonly repository: AttestationRepositoryPort;
@@ -16,6 +19,12 @@ export interface VerifyAttestationDeps {
16
19
  readonly hasher: ContentHasherPort;
17
20
  readonly granularityService: GranularityDerivationService;
18
21
  readonly mapper: AttestationRecordMapper;
22
+ /** H16-03: acBoundScope 再導出用(省略時は空 allowlist として扱い、格納が [] のときのみ合格)。 */
23
+ readonly matrixSource?: MatrixSourcePort;
24
+ readonly allowlist?: AcBoundAllowlistPort;
25
+ readonly acBoundScopeService?: AcBoundScopeService;
26
+ /** H16-03: matrix パスの明示指定(省略時は inputs.sources から解決)。 */
27
+ readonly matrixFilePath?: string;
19
28
  }
20
29
 
21
30
  export interface VerifyAttestationResult {
@@ -96,14 +105,18 @@ export class VerifyAttestationUseCase {
96
105
  mismatches.push("granularity mismatch: stored granularity does not match re-derived value");
97
106
  }
98
107
 
108
+ // 6b. acBoundScope を stored matrix + allowlist から再導出 == 格納値(anti-laundering, H16-03)
109
+ const acBoundScopeOk = await this.checkAcBoundScope(record, mismatches);
110
+
99
111
  const checks: VerifyAttestationChecks = {
100
112
  schema: true,
101
113
  mode: true,
102
114
  attestationDigest: attestationDigestOk,
103
115
  inputHashes: inputHashesOk,
104
116
  granularity: granularityOk,
117
+ acBoundScope: acBoundScopeOk,
105
118
  };
106
- const ok = attestationDigestOk && inputHashesOk && granularityOk;
119
+ const ok = attestationDigestOk && inputHashesOk && granularityOk && acBoundScopeOk;
107
120
 
108
121
  return {
109
122
  output: { ok, checks, mismatches },
@@ -118,10 +131,56 @@ export class VerifyAttestationUseCase {
118
131
  attestationDigest: false,
119
132
  inputHashes: false,
120
133
  granularity: false,
134
+ acBoundScope: false,
121
135
  ...partialChecks,
122
136
  };
123
137
  return { output: { ok: false, checks, mismatches }, exitCode: 2 };
124
138
  }
139
+
140
+ /**
141
+ * H16-03: acBoundScope を stored matrix + config allowlist から再導出し格納値と比較する(anti-laundering)。
142
+ *
143
+ * - ポート/サービスが未配線: 格納 acBoundScope が [] のときのみ合格(後方互換。空 allowlist 相当)。
144
+ * - matrix/allowlist が読めない・parse 不能: fail-closed(false, Q2)。
145
+ * - matrix パスは inputs.sources(ハッシュ検証済み入力)から解決する(Q3)。
146
+ */
147
+ private async checkAcBoundScope(record: AttestationRecord, mismatches: string[]): Promise<boolean> {
148
+ const stored = [...record.acBoundScope].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
149
+
150
+ if (!this.deps.matrixSource || !this.deps.allowlist || !this.deps.acBoundScopeService) {
151
+ // 未配線: 格納が空のときのみ合格(空 allowlist で再導出すれば [] になるため)。
152
+ if (stored.length === 0) return true;
153
+ mismatches.push("acBoundScope check unavailable: matrix source not wired but stored acBoundScope is non-empty");
154
+ return false;
155
+ }
156
+
157
+ let rederived: string[];
158
+ try {
159
+ const allowlist = await this.deps.allowlist.getAcBoundStories();
160
+ const matrixPath = this.deps.matrixFilePath ?? this.resolveMatrixPath(record);
161
+ const matrix = await this.deps.matrixSource.load(matrixPath);
162
+ rederived = this.deps.acBoundScopeService.derive(matrix, allowlist);
163
+ } catch (e) {
164
+ // FAIL-CLOSED(Q2): 再導出入力が読めない/parse 不能は不一致として扱う。
165
+ mismatches.push(`acBoundScope re-derivation failed (fail-closed): ${errMsg(e)}`);
166
+ return false;
167
+ }
168
+
169
+ const rederivedSorted = [...rederived].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
170
+ const equal = stored.length === rederivedSorted.length && stored.every((s, i) => s === rederivedSorted[i]);
171
+ if (!equal) {
172
+ mismatches.push(
173
+ `acBoundScope mismatch: stored [${stored.join(",")}], re-derived [${rederivedSorted.join(",")}]`,
174
+ );
175
+ }
176
+ return equal;
177
+ }
178
+
179
+ /** inputs.sources から matrix にあたるパスを解決する(Q3)。 */
180
+ private resolveMatrixPath(record: AttestationRecord): string | undefined {
181
+ const matrixEntry = record.inputs.sources.find((s) => s.path.includes("requirement-test-matrix"));
182
+ return matrixEntry?.path;
183
+ }
125
184
  }
126
185
 
127
186
  function errMsg(e: unknown): string {
@@ -13,9 +13,12 @@ import { AttestationRecordMapper } from "./application/mappers/attestation-recor
13
13
  import { ProduceAttestationUseCase } from "./application/usecases/produce-attestation-usecase.js";
14
14
  import { VerifyAttestationUseCase } from "./application/usecases/verify-attestation-usecase.js";
15
15
  import { GranularityDerivationService } from "./domain/services/granularity-derivation-service.js";
16
+ import { AcBoundScopeService } from "./domain/services/ac-bound-scope-service.js";
16
17
  import { CiCheckGateResultAdapter } from "./infrastructure/adapters/ci-check-gate-result-adapter.js";
17
18
  import { FileSystemAttestationRepositoryAdapter } from "./infrastructure/adapters/file-system-attestation-repository-adapter.js";
18
19
  import { FileSystemSourceDigesterAdapter } from "./infrastructure/adapters/file-system-source-digester-adapter.js";
20
+ import { FileSystemMatrixSourceAdapter } from "./infrastructure/adapters/file-system-matrix-source-adapter.js";
21
+ import { ConfigAcBoundAllowlistAdapter } from "./infrastructure/adapters/config-ac-bound-allowlist-adapter.js";
19
22
  import { NodeCryptoContentHasherAdapter } from "./infrastructure/adapters/node-crypto-content-hasher-adapter.js";
20
23
  import { AttestHandler } from "./presentation/handlers/attest-handler.js";
21
24
  import { VerifyAttestationHandler } from "./presentation/handlers/verify-attestation-handler.js";
@@ -51,8 +54,13 @@ export function createAttestationModule(rootDir: string, options?: AttestationMo
51
54
 
52
55
  // Domain services / mappers
53
56
  const granularityService = new GranularityDerivationService();
57
+ const acBoundScopeService = new AcBoundScopeService();
54
58
  const mapper = new AttestationRecordMapper();
55
59
 
60
+ // H16-03: acBoundScope 導出用 adapter(matrix 供給 + config allowlist)。
61
+ const matrixSource = new FileSystemMatrixSourceAdapter(rootDir);
62
+ const allowlist = new ConfigAcBoundAllowlistAdapter();
63
+
56
64
  const gitCommitProvider = options?.gitCommitProvider ?? (() => defaultGitCommitProvider(rootDir));
57
65
 
58
66
  // Use cases
@@ -66,6 +74,9 @@ export function createAttestationModule(rootDir: string, options?: AttestationMo
66
74
  gitCommitProvider,
67
75
  pkgVersion: options?.pkgVersion ?? "0.0.0",
68
76
  clock: options?.clock,
77
+ matrixSource,
78
+ allowlist,
79
+ acBoundScopeService,
69
80
  });
70
81
  const verifyUseCase = new VerifyAttestationUseCase({
71
82
  repository,
@@ -73,6 +84,9 @@ export function createAttestationModule(rootDir: string, options?: AttestationMo
73
84
  hasher,
74
85
  granularityService,
75
86
  mapper,
87
+ matrixSource,
88
+ allowlist,
89
+ acBoundScopeService,
76
90
  });
77
91
 
78
92
  // Handlers
@@ -41,6 +41,12 @@ export interface AttestationRecordProps {
41
41
  readonly granularity: { readonly traceability: GranularityClaim };
42
42
  readonly metadata: MetadataSection;
43
43
  readonly signature: SignatureBlock;
44
+ /**
45
+ * H16-03 / WI-227: 実際に ac-bound かつ L3-005 スコープ内で pass した story-id(昇順)。
46
+ * canonical payload に含まれ attestationDigest でカバーされる。granularity.level とは独立。
47
+ * 省略時は [] として扱う(additive-safe)。
48
+ */
49
+ readonly acBoundScope?: readonly string[];
44
50
  }
45
51
 
46
52
  /**
@@ -87,6 +93,7 @@ export class AttestationRecord {
87
93
  readonly granularity: { readonly traceability: GranularityClaim };
88
94
  readonly metadata: MetadataSection;
89
95
  readonly signature: SignatureBlock;
96
+ readonly acBoundScope: readonly string[];
90
97
 
91
98
  private constructor(props: AttestationRecordProps) {
92
99
  this.schemaVersion = props.schemaVersion;
@@ -96,6 +103,7 @@ export class AttestationRecord {
96
103
  this.granularity = props.granularity;
97
104
  this.metadata = props.metadata;
98
105
  this.signature = props.signature;
106
+ this.acBoundScope = Object.freeze([...(props.acBoundScope ?? [])]);
99
107
  Object.freeze(this);
100
108
  }
101
109
 
@@ -185,6 +193,8 @@ export class AttestationRecord {
185
193
  knownLimitations: [...this.granularity.traceability.knownLimitations],
186
194
  },
187
195
  },
196
+ // H16-03: acBoundScope は canonical payload に含める(attestationDigest でカバー)。
197
+ acBoundScope: [...this.acBoundScope],
188
198
  metadata: {
189
199
  producer: this.metadata.producer,
190
200
  },
@@ -211,6 +221,7 @@ export class AttestationRecord {
211
221
  granularity: this.granularity,
212
222
  metadata: this.metadata,
213
223
  signature: SignatureBlock.unsignedPoc(digest),
224
+ acBoundScope: this.acBoundScope,
214
225
  });
215
226
  }
216
227
 
@@ -234,6 +245,8 @@ export class AttestationRecord {
234
245
  return false;
235
246
  }
236
247
  if (!this.granularity.traceability.equals(other.granularity.traceability)) return false;
248
+ if (this.acBoundScope.length !== other.acBoundScope.length) return false;
249
+ if (!this.acBoundScope.every((s, i) => s === other.acBoundScope[i])) return false;
237
250
  if (!this.signature.equals(other.signature)) return false;
238
251
  return true;
239
252
  }
@@ -0,0 +1,80 @@
1
+ // @unit attestation
2
+ // @layer domain
3
+
4
+ /**
5
+ * AcBoundScopeService(H16-03 / WI-227)
6
+ *
7
+ * requirement-test-matrix + allowlist から acBoundScope を機械導出する純粋・決定論サービス。
8
+ * 生成(produce)と検証(verify anti-laundering 再導出)で完全に同一の結果を返す(option-a determinism)。
9
+ *
10
+ * 資格条件(domain_model INV-8): story が返り値に含まれる ⟺
11
+ * (1) allowlist に含まれ、かつ
12
+ * (2) その story の全 linked AC が ≥1 の `binding:"ac"` テスト参照を持つ(fileFallbackOnly===0)。
13
+ *
14
+ * anti-laundering: 導出は stored matrix + config allowlist のみを真実の源とする。格納 acBoundScope を
15
+ * 過大主張へ改竄しても、再導出は matrix/allowlist の honest な結果を返すため mismatch が検出される。
16
+ *
17
+ * `granularity.traceability.level`("file")とは完全に独立した別次元の主張である。
18
+ */
19
+
20
+ interface MatrixTestReference {
21
+ readonly binding?: string;
22
+ }
23
+
24
+ interface MatrixAcMapping {
25
+ readonly testReferences?: readonly MatrixTestReference[];
26
+ }
27
+
28
+ interface MatrixStory {
29
+ readonly storyId?: string;
30
+ readonly storyMappings?: readonly MatrixAcMapping[];
31
+ readonly acMappings?: readonly MatrixAcMapping[];
32
+ }
33
+
34
+ export class AcBoundScopeService {
35
+ /**
36
+ * @param matrix requirement-test-matrix(parse 済み plain object)
37
+ * @param allowlist スコープ対象 story-id
38
+ * @returns 資格を満たす story-id の昇順ソート配列
39
+ */
40
+ derive(matrix: unknown, allowlist: readonly string[]): string[] {
41
+ const scope = new Set(allowlist);
42
+ if (scope.size === 0) return [];
43
+
44
+ const stories = this.extractStories(matrix);
45
+ const qualified: string[] = [];
46
+
47
+ for (const story of stories) {
48
+ const storyId = story.storyId ?? "";
49
+ if (!scope.has(storyId)) continue;
50
+
51
+ const acMappings = story.storyMappings ?? story.acMappings ?? [];
52
+ // linked AC が 1 件も無い場合は資格対象外(検証すべき AC が存在しない)。
53
+ if (acMappings.length === 0) continue;
54
+
55
+ const everyAcBound = acMappings.every((ac) => {
56
+ const refs = ac.testReferences ?? [];
57
+ // 未リンク AC(refs 空)があれば ac-bound 主張は成立しない。
58
+ if (refs.length === 0) return false;
59
+ return refs.some((ref) => ref.binding === "ac");
60
+ });
61
+
62
+ if (everyAcBound) {
63
+ qualified.push(storyId);
64
+ }
65
+ }
66
+
67
+ return qualified.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
68
+ }
69
+
70
+ private extractStories(matrix: unknown): readonly MatrixStory[] {
71
+ if (typeof matrix !== "object" || matrix === null) return [];
72
+ const obj = matrix as { stories?: unknown; storyMappings?: unknown };
73
+ const raw = Array.isArray(obj.stories)
74
+ ? obj.stories
75
+ : Array.isArray(obj.storyMappings)
76
+ ? obj.storyMappings
77
+ : [];
78
+ return raw as readonly MatrixStory[];
79
+ }
80
+ }
@@ -0,0 +1,23 @@
1
+ // @unit attestation
2
+ // @layer infrastructure
3
+
4
+ import type { AcBoundAllowlistPort } from "../../application/ports/ac-bound-allowlist-port.js";
5
+
6
+ /**
7
+ * AcBoundAllowlistPort の config-foundation 実装(H16-03 / WI-227)。
8
+ * resolved config の `layers.L3.acBoundStories`(既定 [])を供給する。
9
+ * config 不在・取得失敗時は [] を返す(acBoundScope は空になる)。
10
+ */
11
+ export class ConfigAcBoundAllowlistAdapter implements AcBoundAllowlistPort {
12
+ async getAcBoundStories(): Promise<readonly string[]> {
13
+ try {
14
+ const { createConfigFoundationModule } = await import("../../../config-foundation/composition-root.js");
15
+ const configModule = createConfigFoundationModule();
16
+ const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
17
+ const l3 = resolvedConfig.config.layers.L3 as { acBoundStories?: readonly string[] };
18
+ return l3.acBoundStories ?? [];
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,25 @@
1
+ // @unit attestation
2
+ // @layer infrastructure
3
+
4
+ import { readFile } from "node:fs/promises";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import type { MatrixSourcePort } from "../../application/ports/matrix-source-port.js";
7
+
8
+ /**
9
+ * MatrixSourcePort の node:fs 実装(H16-03 / WI-227)。
10
+ * requirement-test-matrix.json を読み込み parse して返す。
11
+ * 不在・parse 不能は throw する(呼び出し側 usecase が fail-closed / [] に変換する)。
12
+ */
13
+ export class FileSystemMatrixSourceAdapter implements MatrixSourcePort {
14
+ constructor(
15
+ private readonly baseDir: string,
16
+ private readonly defaultMatrixPath = ".harness/requirement-test-matrix.json",
17
+ ) {}
18
+
19
+ async load(matrixFilePath?: string): Promise<unknown> {
20
+ const rel = matrixFilePath && matrixFilePath.length > 0 ? matrixFilePath : this.defaultMatrixPath;
21
+ const absPath = isAbsolute(rel) ? rel : resolve(this.baseDir, rel);
22
+ const raw = await readFile(absPath, "utf8");
23
+ return JSON.parse(raw);
24
+ }
25
+ }
@@ -55,6 +55,7 @@ function renderHuman(output: VerifyAttestationOutput, exitCode: 0 | 1 | 2): stri
55
55
  lines.push(`attestationDigest: ${mark(c.attestationDigest)}`);
56
56
  lines.push(`inputHashes : ${mark(c.inputHashes)}`);
57
57
  lines.push(`granularity : ${mark(c.granularity)}`);
58
+ lines.push(`acBoundScope : ${mark(c.acBoundScope)}`);
58
59
  if (output.mismatches.length > 0) {
59
60
  lines.push("");
60
61
  lines.push("Mismatches:");