phasegate 0.162.0 → 0.165.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 +11 -0
- package/docs/ADR/019-ai-independence-boundary.md +63 -0
- package/docs/ADR/020-reverse-learning-forward-proposal.md +52 -0
- package/package.json +1 -1
- package/scripts/harness/attestation/application/dto/attestation-document.ts +54 -0
- package/scripts/harness/attestation/application/dto/produce-attestation-input.ts +18 -0
- package/scripts/harness/attestation/application/dto/verify-attestation-input.ts +12 -0
- package/scripts/harness/attestation/application/dto/verify-attestation-output.ts +30 -0
- package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +240 -0
- package/scripts/harness/attestation/application/ports/attestation-repository-port.ts +14 -0
- package/scripts/harness/attestation/application/ports/gate-result-source-port.ts +23 -0
- package/scripts/harness/attestation/application/ports/source-digester-port.ts +15 -0
- package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +155 -0
- package/scripts/harness/attestation/application/usecases/verify-attestation-usecase.ts +137 -0
- package/scripts/harness/attestation/composition-root.ts +93 -0
- package/scripts/harness/attestation/domain/entities/attestation-record.ts +240 -0
- package/scripts/harness/attestation/domain/ports/content-hasher-port.ts +13 -0
- package/scripts/harness/attestation/domain/services/granularity-derivation-service.ts +51 -0
- package/scripts/harness/attestation/domain/value-objects/digest.ts +51 -0
- package/scripts/harness/attestation/domain/value-objects/granularity-claim.ts +89 -0
- package/scripts/harness/attestation/domain/value-objects/signature-block.ts +87 -0
- package/scripts/harness/attestation/domain/value-objects/validator-outcome.ts +44 -0
- package/scripts/harness/attestation/index.ts +18 -0
- package/scripts/harness/attestation/infrastructure/adapters/ci-check-gate-result-adapter.ts +164 -0
- package/scripts/harness/attestation/infrastructure/adapters/file-system-attestation-repository-adapter.ts +32 -0
- package/scripts/harness/attestation/infrastructure/adapters/file-system-source-digester-adapter.ts +27 -0
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +18 -0
- package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +71 -0
- package/scripts/harness/attestation/presentation/handlers/verify-attestation-handler.ts +74 -0
- package/scripts/harness/main.ts +51 -2
- package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.165.0] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **WI-224 — 手1 signed-attestation PoC (`attestation` unit, unsigned-poc mode)** — adds a new additive `attestation` unit (Clean Architecture: domain / application / infrastructure / presentation) that produces and mechanically re-verifies a content-addressed attestation document for a `phasegate:ci-check` gate run. Purely additive: it does not modify or run inside any existing gate (ADR-020 compliant).
|
|
15
|
+
- **2 つの opt-in CLI コマンド**: `phasegate:attest`(`phasegate:ci-check` の結果から署名付き attestation ドキュメントを生成)/ `phasegate:verify-attestation <file>`(生成済みドキュメントを鍵なしで機械的に再検証)。既存ゲートの経路には注入しない独立コマンド。
|
|
16
|
+
- **記録内容**: validator-set + per-validator result(`{ validatorId, passed, skipped }`)+ 入力 digest(`phasegate.config.json` / requirement-test-matrix / git SHA)+ **granularity / known-limitations ブロック**。granularity は L3-004 のトレーサビリティが **FILE-LEVEL(per-AC ではない)** であることを機械的に記録し、粗い green を細かい保証として詐称(laundering)できないようにする。
|
|
17
|
+
- **`unsigned-poc` signature モード**: content-addressed self-digest(`attestationDigest`)で **改竄検知 / INTEGRITY のみ** を証明する(発行者の真正性 / AUTHENTICITY は証明しない)。`signature.mode` discriminator を予約し、後から本物の ed25519 署名を差し込めるようにした。`--mode signed` は現状 "not yet implemented"(exit 2)。
|
|
18
|
+
- **決定論**: `attestationDigest` は signature ブロックと volatile な metadata(`producedAt` / `gitCommit`)を除去した canonical payload 上で算出するため verify が再現可能。attestation の既定出力先は `.harness/attestation.json`(gitignore 済み)。
|
|
19
|
+
- ストーリー H16-01 / H16-02 を追加。L3-004 のトレーサビリティは引き続き green(Missing tests: 0)。
|
|
20
|
+
|
|
10
21
|
## [0.162.0] - 2026-07-04
|
|
11
22
|
|
|
12
23
|
### Fixed
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
adr_id: "019"
|
|
3
|
+
title: "「AI 非依存」の境界 — trust 境界は機械的、AI は生成のみ"
|
|
4
|
+
status: Accepted
|
|
5
|
+
date: 2026-07-05
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 「AI 非依存」の境界 — trust 境界は機械的、AI は生成のみ
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
ADR-006(エージェント非依存設計原則)は、全バリデータが **ファイルシステムに書き込まれた成果物のみ**(path / 内容 / AST)を検査対象とし、エージェント固有の API に依存しないことを定めた。
|
|
13
|
+
|
|
14
|
+
2026-07-04 の監査メモ(`docs/inception/_shared/security_audit_2026-07-04.md` §1)は、この「AI 非依存」の**粒度が曖昧**であることを指摘した。
|
|
15
|
+
|
|
16
|
+
- 「品質ルールが agent 非依存」は真である。
|
|
17
|
+
- しかし「全機能が agent 非依存」は偽である。phasegate 自身が、意味のある成果物を **生成する** ための AI 駆動・phasegate 固有スキル(`story-implementor`, `quick-implementor` など)を既に同梱しており、これらは AI エージェントが実行する。すなわち **生成側には既に AI が存在する**。
|
|
18
|
+
|
|
19
|
+
一方、汎用化ロードマップが挙げるセマンティック系のアイデア(semantic triple graph / AC 単位のトレーサビリティ、name-equivalence / ユビキタス言語レジストリ、rationale 必須化)は、素朴に LLM 推論へ寄せると、次のいずれかのリスクを負う。
|
|
20
|
+
|
|
21
|
+
- AI 非依存という価値提案そのものを壊す(検証に AI 判断が混入する)
|
|
22
|
+
- あるいは著者に過大な記述負担を課す
|
|
23
|
+
|
|
24
|
+
新しい先例として **artifact-conformance test**(例: `scripts/harness/__tests__/unit/skill-quality/quick-implementor-skill-conformance.test.ts`、story H10-04)がある。これは markdown 成果物が特定の必須ディレクティブを含むかを **LLM を一切使わず機械的に** アサートするものであり、機械的(= AI 非依存)検証が raw AST よりさらに遠くまで届くことの証左である。
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
「AI 非依存」を、全機能の性質ではなく **trust 境界(バリデータ / ゲート / 今後導入する attestation)の性質** として厳密に再定義する。この命名を明示することで、監査が指摘した曖昧さを解消する。
|
|
29
|
+
|
|
30
|
+
1. **trust を付与する検査 — attestable な「green」を生み得るすべて — は、厳密に機械的かつ再現可能であり続ける。** 対象は path / 内容 / AST / content-conformance アサーション、およびユーザーが記述したレジストリ・アノテーションに対する整合性検査である。**LLM / AI 判断が検証(validation)の一部になることは決してない。**
|
|
31
|
+
|
|
32
|
+
2. **AI は生成側で許容され、かつ期待される。** phasegate 固有スキルが成果物を生成する用途、および著者の補助として AI を使ってよい。ただし AI が生成したすべての成果物は、その後に機械的ゲートで検証される。**AI が trust を付与することはない。**
|
|
33
|
+
|
|
34
|
+
3. **セマンティックな要件(name-equivalence、intent / rationale)は、ユーザーが記述したデータ** — ユビキタス言語レジストリ、AC 単位のアノテーション、ADR / WI リンクの必須化 — として表現し、それを **機械的に** 検査することで機械化する。**LLM 推論では機械化しない。**
|
|
35
|
+
|
|
36
|
+
4. 本決定は **ADR-006 を維持・先鋭化するもの**であり、そのスコープを精緻化する(supersede しない)。
|
|
37
|
+
|
|
38
|
+
5. 明示的に、**AI 補助のバリデータ tier は導入しない。** 将来あるセマンティック検査がそれを正当化する場合でも、それ自身の tightly-scoped な ADR を必要とし、non-blocking(advisory)でなければならず、attestation から除外されなければならない。
|
|
39
|
+
|
|
40
|
+
位置づけの要約:
|
|
41
|
+
|
|
42
|
+
| 側 | AI の可否 | trust の源泉 |
|
|
43
|
+
|----|----------|-------------|
|
|
44
|
+
| 生成(成果物の作成・著者補助) | 許容・期待 | trust は付与しない |
|
|
45
|
+
| 検証(ゲート / attestation) | 不可 | 機械的・再現可能な検査のみ |
|
|
46
|
+
|
|
47
|
+
## Consequences
|
|
48
|
+
|
|
49
|
+
- attestation が絶対的かつ再現可能になる。誰でも AI なしで「green」を再検証できる。
|
|
50
|
+
- セマンティックカバレッジは、レジストリ / アノテーションが表現できる範囲に有界となる(著者の記述負担を受け入れる)。
|
|
51
|
+
- 「チェッカーは常に機械である」というアイデンティティが保たれる。
|
|
52
|
+
- モデル / バージョンのドリフトでゲートが flaky になることがない。
|
|
53
|
+
- 計画中の署名付き attestation(signed-attestation)ワークを可能にする土台となる。
|
|
54
|
+
|
|
55
|
+
## Alternatives
|
|
56
|
+
|
|
57
|
+
1. **現状維持** — 曖昧なまま「AI 非依存」を全機能の性質として語る。監査が指摘した矛盾を放置するため不採用。
|
|
58
|
+
|
|
59
|
+
2. **機械的ゲート + non-blocking な AI-advisory バリデータ tier** — 現時点では不採用。理由は (a) gate-creep のリスク(advisory が事実上のゲート化する)、(b) AI の findings は再現不能で attestation を毀損する。将来の tightly-scoped なオプションとしてのみ残す。
|
|
60
|
+
|
|
61
|
+
3. **原則を「機械的に検証可能なら AI も許容(AI の出力が決定的に検査できる限り)」へ再定義** — 中核的な価値提案に対する変更が大きすぎるため不採用。
|
|
62
|
+
|
|
63
|
+
関連: ADR-006(エージェント非依存設計原則)、ADR-020(単方向規律と逆流学習)、`docs/inception/_shared/security_audit_2026-07-04.md` §1。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
adr_id: "020"
|
|
3
|
+
title: "単方向規律と逆流学習 — 逆流は前方草案を提案し、人が承認する"
|
|
4
|
+
status: Accepted
|
|
5
|
+
date: 2026-07-05
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 単方向規律と逆流学習 — 逆流は前方草案を提案し、人が承認する
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
`docs/folder_management_rules.md` §2 は、inception → product → src(実装)という **単方向のデータフロー** を定め、「逆流はフェーズゲートでブロックされます」と明記している。フェーズ依存レベル L1 → L2 → L3 は Quick Mode でも緩和不可である。
|
|
13
|
+
|
|
14
|
+
現時点で存在する唯一の「逆流的」機構は L4-001 `drift-detect` のみである。これは advisory・default-off・観測的(warning-only)であり、強制も自動更新も行わない(ADR-017 / ADR-018 / `docs/guide/layer-model.md` §L4 参照)。
|
|
15
|
+
|
|
16
|
+
汎用化ロードマップのアイデア (g) は「逆流学習(reverse learning)」— コードからの学習(design-smell 検出、pattern learning)— を求めている。ここには **逆流が「まずコードを書き、後から設計を埋める(code-first, backfill design later)」への裏口になる** リスクがある。それはフェーズゲートの中核規律を溶かす。
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
単方向の **強制(enforcement)は維持され、交渉の余地はない。** 逆流が単一源(product / inception 文書)を直接書き換えることは決してなく、前方ゲート(forward gate)を緩和することも決してない。
|
|
21
|
+
|
|
22
|
+
逆流学習は、次の形でのみ許容する。
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
検出(detect) : drift / smell を機械的に、または advisory な L4 層で表面化
|
|
26
|
+
↓
|
|
27
|
+
前方草案の生成(propose) : inception の WI または ADR の草案(draft)を生成
|
|
28
|
+
↓
|
|
29
|
+
人が承認(approve) : 承認された草案が通常の前方フローに再投入される
|
|
30
|
+
↓
|
|
31
|
+
機械的な前方ゲートで検証 : 他のいかなる設計変更とも同じゲートを通過する
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- 逆流が生み出すのは **提案(proposal)のみ** である。ゲートを迂回するものは何もない。これにより単方向性を壊すことなく学習ループを閉じる。
|
|
35
|
+
- **ADR-019 との整合**: 草案生成のステップは AI スキル(生成側)を用いてよい。しかし trust は **人の承認 + 機械的な前方ゲート** が付与するのであって、AI が付与するのではない。
|
|
36
|
+
- **L4 との関係**: 検出は L4-001 `drift-detect` を拡張しうるが、L4-001 は **advisory / default-off のまま** である。本決定は L4 を blocking へ昇格させない。新しい要素は「前方草案の提案 + 人の承認」経路のみである。
|
|
37
|
+
|
|
38
|
+
## Consequences
|
|
39
|
+
|
|
40
|
+
- 「コードから学ぶ」という価値を取り込みつつ、背骨(単方向規律)を無傷に保つ。
|
|
41
|
+
- 提案 / 承認の機構が加わる(machinery のコスト)。rubber-stamp(形式承認)のリスクは、承認された草案が **他の設計変更と同じ前方ゲートを通過することを必須とする** ことで緩和する。
|
|
42
|
+
- 単一源の自動書き換え(auto-mutation)は行わない。逆流学習の恩恵は「人の承認を経た草案」に限られる。
|
|
43
|
+
|
|
44
|
+
## Alternatives
|
|
45
|
+
|
|
46
|
+
1. **逆流を advisory のみに留め、提案は行わない(検出 + 報告のみ)** — 能力が低く学習ループを閉じられないため不採用。
|
|
47
|
+
|
|
48
|
+
2. **前方草案の提案フロー(本決定で採用)** — 検出 → 前方草案の提案 → 人の承認 → 前方ゲートでの検証。
|
|
49
|
+
|
|
50
|
+
3. **双方向フローを全面的に排除する** — 「コードから学ぶ」価値を放棄することになるため不採用。
|
|
51
|
+
|
|
52
|
+
関連: ADR-019(「AI 非依存」の境界)、ADR-017(warning severity の集計)、ADR-018(drift-detect の design pointers)、`docs/folder_management_rules.md` §2、`docs/guide/layer-model.md` §L4。
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* record format `phasegate-attestation/v1` の plain object 表現(logical_design §1.4.3)。
|
|
6
|
+
* ファイル入出力・stdout エコー・mapper の境界で用いる契約型。VO はプリミティブ展開する。
|
|
7
|
+
*/
|
|
8
|
+
export interface AttestationDocumentValidatorOutcome {
|
|
9
|
+
readonly validatorId: string;
|
|
10
|
+
readonly passed: boolean;
|
|
11
|
+
readonly skipped: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AttestationDocumentSource {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly digest: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AttestationDocumentGranularityClaim {
|
|
20
|
+
readonly validator: string;
|
|
21
|
+
readonly level: "file" | "ac";
|
|
22
|
+
readonly claim: string;
|
|
23
|
+
readonly knownLimitations: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AttestationDocument {
|
|
27
|
+
readonly schemaVersion: "phasegate-attestation/v1";
|
|
28
|
+
readonly predicateType: string;
|
|
29
|
+
readonly subject: {
|
|
30
|
+
readonly command: string;
|
|
31
|
+
readonly gateResult: "pass" | "fail";
|
|
32
|
+
readonly validatorSet: AttestationDocumentValidatorOutcome[];
|
|
33
|
+
};
|
|
34
|
+
readonly inputs: {
|
|
35
|
+
readonly digestAlgorithm: "sha256";
|
|
36
|
+
readonly sources: AttestationDocumentSource[];
|
|
37
|
+
readonly inputDigest: string;
|
|
38
|
+
};
|
|
39
|
+
readonly granularity: {
|
|
40
|
+
readonly traceability: AttestationDocumentGranularityClaim;
|
|
41
|
+
};
|
|
42
|
+
readonly metadata: {
|
|
43
|
+
readonly producedAt: string;
|
|
44
|
+
readonly producer: string;
|
|
45
|
+
readonly gitCommit: string | null;
|
|
46
|
+
};
|
|
47
|
+
readonly signature: {
|
|
48
|
+
readonly mode: "unsigned-poc" | "signed";
|
|
49
|
+
readonly attestationDigest: string;
|
|
50
|
+
readonly algorithm: string | null;
|
|
51
|
+
readonly keyId: string | null;
|
|
52
|
+
readonly value: string | null;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import type { SignatureMode } from "../../domain/value-objects/signature-block.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* attest usecase 入力 DTO(logical_design §4.1)。
|
|
8
|
+
*/
|
|
9
|
+
export interface ProduceAttestationInput {
|
|
10
|
+
/** record 出力先パス(既定は presentation で `.harness/attestation.json`)。 */
|
|
11
|
+
readonly out: string;
|
|
12
|
+
/** gateResult != "pass" なら record を一切出力せず exit 1。 */
|
|
13
|
+
readonly requirePass: boolean;
|
|
14
|
+
/** 生成 document を stdout へエコーするか。 */
|
|
15
|
+
readonly emitJson: boolean;
|
|
16
|
+
/** 署名モード。`signed` は not-yet-implemented。 */
|
|
17
|
+
readonly mode: SignatureMode;
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* verify usecase 入力 DTO(logical_design §4.1)。
|
|
6
|
+
*/
|
|
7
|
+
export interface VerifyAttestationInput {
|
|
8
|
+
/** 検証対象の attestation ファイルパス。 */
|
|
9
|
+
readonly filePath: string;
|
|
10
|
+
/** 各チェック結果を機械可読に出力するか。 */
|
|
11
|
+
readonly emitJson: boolean;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 機械的 5 チェックの個別結果(logical_design §1.4.4 / §4.1)。
|
|
6
|
+
*/
|
|
7
|
+
export interface VerifyAttestationChecks {
|
|
8
|
+
/** schema/shape 妥当 */
|
|
9
|
+
readonly schema: boolean;
|
|
10
|
+
/** mode サポート対象(signed は非対応) */
|
|
11
|
+
readonly mode: boolean;
|
|
12
|
+
/** canonical payload 上で attestationDigest 再計算 == 格納値 */
|
|
13
|
+
readonly attestationDigest: boolean;
|
|
14
|
+
/** inputs.sources[].digest を現在ファイルから再計算 == 格納値 */
|
|
15
|
+
readonly inputHashes: boolean;
|
|
16
|
+
/** granularity を validatorSet から再導出 == 格納値(anti-laundering) */
|
|
17
|
+
readonly granularity: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* verify usecase 出力 DTO(logical_design §4.1)。
|
|
22
|
+
*/
|
|
23
|
+
export interface VerifyAttestationOutput {
|
|
24
|
+
/** 全チェック合格か。 */
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
/** 各機械的チェックの合否。 */
|
|
27
|
+
readonly checks: VerifyAttestationChecks;
|
|
28
|
+
/** mismatch / エラーの説明群。 */
|
|
29
|
+
readonly mismatches: string[];
|
|
30
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AttestationRecord,
|
|
6
|
+
type AttestationRecordProps,
|
|
7
|
+
type GateResult,
|
|
8
|
+
} from "../../domain/entities/attestation-record.js";
|
|
9
|
+
import { Digest } from "../../domain/value-objects/digest.js";
|
|
10
|
+
import { GranularityClaim } from "../../domain/value-objects/granularity-claim.js";
|
|
11
|
+
import { SignatureBlock } from "../../domain/value-objects/signature-block.js";
|
|
12
|
+
import { ValidatorOutcome } from "../../domain/value-objects/validator-outcome.js";
|
|
13
|
+
import type { AttestationDocument, AttestationDocumentGranularityClaim } from "../dto/attestation-document.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* verify 時に外部 JSON の shape/型が不正なとき送出する例外。
|
|
17
|
+
* errorCode: L1-053(logical_design §2.4)
|
|
18
|
+
*/
|
|
19
|
+
export class MalformedAttestationError extends Error {
|
|
20
|
+
readonly errorCode = "L1-053";
|
|
21
|
+
|
|
22
|
+
constructor(message: string) {
|
|
23
|
+
super(`Malformed attestation: ${message} [L1-053]`);
|
|
24
|
+
this.name = "MalformedAttestationError";
|
|
25
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
30
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function requireString(v: unknown, field: string): string {
|
|
34
|
+
if (typeof v !== "string") {
|
|
35
|
+
throw new MalformedAttestationError(`${field} must be a string`);
|
|
36
|
+
}
|
|
37
|
+
return v;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireBoolean(v: unknown, field: string): boolean {
|
|
41
|
+
if (typeof v !== "boolean") {
|
|
42
|
+
throw new MalformedAttestationError(`${field} must be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* AttestationRecord(domain)↔ AttestationDocument(application DTO)の双方向変換。
|
|
49
|
+
*/
|
|
50
|
+
export class AttestationRecordMapper {
|
|
51
|
+
/** 集約を plain object へ射影(VO をプリミティブ展開)。 */
|
|
52
|
+
toDocument(record: AttestationRecord): AttestationDocument {
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion: record.schemaVersion as "phasegate-attestation/v1",
|
|
55
|
+
predicateType: record.predicateType,
|
|
56
|
+
subject: {
|
|
57
|
+
command: record.subject.command,
|
|
58
|
+
gateResult: record.subject.gateResult,
|
|
59
|
+
validatorSet: record.subject.validatorSet.map((o) => ({
|
|
60
|
+
validatorId: o.validatorId,
|
|
61
|
+
passed: o.passed,
|
|
62
|
+
skipped: o.skipped,
|
|
63
|
+
})),
|
|
64
|
+
},
|
|
65
|
+
inputs: {
|
|
66
|
+
digestAlgorithm: "sha256",
|
|
67
|
+
sources: record.inputs.sources.map((s) => ({ path: s.path, digest: s.digest.value })),
|
|
68
|
+
inputDigest: record.inputs.inputDigest.value,
|
|
69
|
+
},
|
|
70
|
+
granularity: {
|
|
71
|
+
traceability: {
|
|
72
|
+
validator: record.granularity.traceability.validator,
|
|
73
|
+
level: record.granularity.traceability.level,
|
|
74
|
+
claim: record.granularity.traceability.claim,
|
|
75
|
+
knownLimitations: [...record.granularity.traceability.knownLimitations],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
metadata: {
|
|
79
|
+
producedAt: record.metadata.producedAt,
|
|
80
|
+
producer: record.metadata.producer,
|
|
81
|
+
gitCommit: record.metadata.gitCommit,
|
|
82
|
+
},
|
|
83
|
+
signature: {
|
|
84
|
+
mode: record.signature.mode,
|
|
85
|
+
attestationDigest: record.signature.attestationDigest.value,
|
|
86
|
+
algorithm: record.signature.algorithm,
|
|
87
|
+
keyId: record.signature.keyId,
|
|
88
|
+
value: record.signature.value,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* verify 時に外部 JSON を検証しつつ集約へ再構築。shape/型不正は MalformedAttestationError。
|
|
95
|
+
* INV 違反(INV-1/3/6)は AttestationRecord.create が AttestationInvariantError を送出する。
|
|
96
|
+
*/
|
|
97
|
+
fromDocument(doc: unknown): AttestationRecord {
|
|
98
|
+
if (!isObject(doc)) {
|
|
99
|
+
throw new MalformedAttestationError("root must be an object");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const schemaVersion = requireString(doc.schemaVersion, "schemaVersion");
|
|
103
|
+
const predicateType = requireString(doc.predicateType, "predicateType");
|
|
104
|
+
|
|
105
|
+
const subject = doc.subject;
|
|
106
|
+
if (!isObject(subject)) {
|
|
107
|
+
throw new MalformedAttestationError("subject must be an object");
|
|
108
|
+
}
|
|
109
|
+
const command = requireString(subject.command, "subject.command");
|
|
110
|
+
const gateResultRaw = requireString(subject.gateResult, "subject.gateResult");
|
|
111
|
+
if (gateResultRaw !== "pass" && gateResultRaw !== "fail") {
|
|
112
|
+
throw new MalformedAttestationError('subject.gateResult must be "pass" or "fail"');
|
|
113
|
+
}
|
|
114
|
+
const gateResult = gateResultRaw as GateResult;
|
|
115
|
+
if (!Array.isArray(subject.validatorSet)) {
|
|
116
|
+
throw new MalformedAttestationError("subject.validatorSet must be an array");
|
|
117
|
+
}
|
|
118
|
+
const validatorSet = subject.validatorSet.map((raw, i) => {
|
|
119
|
+
if (!isObject(raw)) {
|
|
120
|
+
throw new MalformedAttestationError(`subject.validatorSet[${i}] must be an object`);
|
|
121
|
+
}
|
|
122
|
+
return ValidatorOutcome.create({
|
|
123
|
+
validatorId: requireString(raw.validatorId, `subject.validatorSet[${i}].validatorId`),
|
|
124
|
+
passed: requireBoolean(raw.passed, `subject.validatorSet[${i}].passed`),
|
|
125
|
+
skipped: requireBoolean(raw.skipped, `subject.validatorSet[${i}].skipped`),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const inputs = doc.inputs;
|
|
130
|
+
if (!isObject(inputs)) {
|
|
131
|
+
throw new MalformedAttestationError("inputs must be an object");
|
|
132
|
+
}
|
|
133
|
+
if (inputs.digestAlgorithm !== "sha256") {
|
|
134
|
+
throw new MalformedAttestationError('inputs.digestAlgorithm must be "sha256"');
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(inputs.sources)) {
|
|
137
|
+
throw new MalformedAttestationError("inputs.sources must be an array");
|
|
138
|
+
}
|
|
139
|
+
const sources = inputs.sources.map((raw, i) => {
|
|
140
|
+
if (!isObject(raw)) {
|
|
141
|
+
throw new MalformedAttestationError(`inputs.sources[${i}] must be an object`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
path: requireString(raw.path, `inputs.sources[${i}].path`),
|
|
145
|
+
digest: this.toDigest(raw.digest, `inputs.sources[${i}].digest`),
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
const inputDigest = this.toDigest(inputs.inputDigest, "inputs.inputDigest");
|
|
149
|
+
|
|
150
|
+
const granularity = doc.granularity;
|
|
151
|
+
if (!isObject(granularity) || !isObject(granularity.traceability)) {
|
|
152
|
+
throw new MalformedAttestationError("granularity.traceability must be an object");
|
|
153
|
+
}
|
|
154
|
+
const traceability = this.toGranularityClaim(granularity.traceability);
|
|
155
|
+
|
|
156
|
+
const metadata = doc.metadata;
|
|
157
|
+
if (!isObject(metadata)) {
|
|
158
|
+
throw new MalformedAttestationError("metadata must be an object");
|
|
159
|
+
}
|
|
160
|
+
const gitCommit = metadata.gitCommit === null ? null : requireString(metadata.gitCommit, "metadata.gitCommit");
|
|
161
|
+
|
|
162
|
+
const signature = doc.signature;
|
|
163
|
+
if (!isObject(signature)) {
|
|
164
|
+
throw new MalformedAttestationError("signature must be an object");
|
|
165
|
+
}
|
|
166
|
+
const mode = requireString(signature.mode, "signature.mode");
|
|
167
|
+
const attestationDigest = this.toDigest(signature.attestationDigest, "signature.attestationDigest");
|
|
168
|
+
// mode の非対応(signed)は verify usecase の mode check で判定するため、ここでは
|
|
169
|
+
// SignatureBlock を直接構築せず、mode をそのまま検証用に保持する必要がある。
|
|
170
|
+
// unsigned-poc のみ SignatureBlock を構築できるので、非対応 mode は shape 検証を通し
|
|
171
|
+
// usecase 側へ委譲するため、props を組み立てる際に signature block を後段で扱う。
|
|
172
|
+
const props: AttestationRecordProps = {
|
|
173
|
+
schemaVersion,
|
|
174
|
+
predicateType,
|
|
175
|
+
subject: { command, gateResult, validatorSet },
|
|
176
|
+
inputs: { digestAlgorithm: "sha256", sources, inputDigest },
|
|
177
|
+
granularity: { traceability },
|
|
178
|
+
metadata: {
|
|
179
|
+
producedAt: requireString(metadata.producedAt, "metadata.producedAt"),
|
|
180
|
+
producer: requireString(metadata.producer, "metadata.producer"),
|
|
181
|
+
gitCommit,
|
|
182
|
+
},
|
|
183
|
+
signature: this.toSignatureBlock(mode, attestationDigest, signature),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// verify 用の再構築: INV-1/INV-3 は verify usecase が機械的チェックとして検出するため
|
|
187
|
+
// ここでは強制しない(reconstruct)。shape/型不正のみ MalformedAttestationError として弾く。
|
|
188
|
+
return AttestationRecord.reconstruct(props);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private toDigest(v: unknown, field: string): Digest {
|
|
192
|
+
const raw = requireString(v, field);
|
|
193
|
+
try {
|
|
194
|
+
return Digest.create(raw);
|
|
195
|
+
} catch {
|
|
196
|
+
throw new MalformedAttestationError(`${field} is not a valid sha256 digest`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private toGranularityClaim(raw: Record<string, unknown>): GranularityClaim {
|
|
201
|
+
const level = raw.level;
|
|
202
|
+
if (level !== "file" && level !== "ac") {
|
|
203
|
+
throw new MalformedAttestationError('granularity.traceability.level must be "file" or "ac"');
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(raw.knownLimitations)) {
|
|
206
|
+
throw new MalformedAttestationError("granularity.traceability.knownLimitations must be an array");
|
|
207
|
+
}
|
|
208
|
+
const knownLimitations = raw.knownLimitations.map((k, i) =>
|
|
209
|
+
requireString(k, `granularity.traceability.knownLimitations[${i}]`),
|
|
210
|
+
);
|
|
211
|
+
const claim: AttestationDocumentGranularityClaim = {
|
|
212
|
+
validator: requireString(raw.validator, "granularity.traceability.validator"),
|
|
213
|
+
level,
|
|
214
|
+
claim: requireString(raw.claim, "granularity.traceability.claim"),
|
|
215
|
+
knownLimitations,
|
|
216
|
+
};
|
|
217
|
+
return GranularityClaim.create(claim);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private toSignatureBlock(mode: string, attestationDigest: Digest, raw: Record<string, unknown>): SignatureBlock {
|
|
221
|
+
// unsigned-poc は SignatureBlock で INV-6 を強制。
|
|
222
|
+
// signed(非対応 mode)は SignatureBlock.create が UnsupportedSignatureModeError を送出するが、
|
|
223
|
+
// verify usecase は mode check(exitCode 2)で先に非対応を扱うため、ここでは
|
|
224
|
+
// unsigned-poc のみ通常構築し、それ以外は shape として null 三点組を検証して仮ブロックを組む。
|
|
225
|
+
if (mode === "unsigned-poc") {
|
|
226
|
+
if (raw.algorithm !== null || raw.keyId !== null || raw.value !== null) {
|
|
227
|
+
throw new MalformedAttestationError("unsigned-poc signature must have null algorithm/keyId/value");
|
|
228
|
+
}
|
|
229
|
+
return SignatureBlock.unsignedPoc(attestationDigest);
|
|
230
|
+
}
|
|
231
|
+
// 非対応 mode: shape のみ検証し unsupported として扱えるよう SignatureBlock.create に委譲。
|
|
232
|
+
return SignatureBlock.create({
|
|
233
|
+
mode: mode as "unsigned-poc" | "signed",
|
|
234
|
+
attestationDigest,
|
|
235
|
+
algorithm: (raw.algorithm ?? null) as string | null,
|
|
236
|
+
keyId: (raw.keyId ?? null) as string | null,
|
|
237
|
+
value: (raw.value ?? null) as string | null,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import type { AttestationDocument } from "../dto/attestation-document.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* attestation ファイルの read/write を抽象化する永続化調停ポート(application 所有)。
|
|
8
|
+
* 集約不変条件に関与しないため domain ではなく application に配置する。
|
|
9
|
+
*/
|
|
10
|
+
export interface AttestationRepositoryPort {
|
|
11
|
+
write(path: string, doc: AttestationDocument): Promise<void>;
|
|
12
|
+
/** parse 済み plain object を返す。不在/parse 失敗は throw し usecase が exitCode 2 へ変換する。 */
|
|
13
|
+
read(path: string): Promise<unknown>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ci-check の1バリデータ結果を写す plain DTO(Shared Kernel の型 import は避ける)。
|
|
6
|
+
*/
|
|
7
|
+
export interface GateValidatorResult {
|
|
8
|
+
readonly validatorId: string;
|
|
9
|
+
readonly passed: boolean;
|
|
10
|
+
readonly skipped: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* gate 実行結果を取得する調停ポート(application 所有)。
|
|
15
|
+
* black-box observation: 実体は subprocess `phasegate:ci-check --json`(infrastructure)。
|
|
16
|
+
* 集約不変条件に関与しないため domain ではなく application に配置する。
|
|
17
|
+
*/
|
|
18
|
+
export interface GateResultSourcePort {
|
|
19
|
+
fetchGateResult(): Promise<{
|
|
20
|
+
readonly allPassed: boolean;
|
|
21
|
+
readonly validatorResults: readonly GateValidatorResult[];
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @unit attestation
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import type { Digest } from "../../domain/value-objects/digest.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* source パスの「現在の」内容から sha256 Digest を供給する調停ポート(application 所有)。
|
|
8
|
+
* 生成時は inputs.sources の digest 構築に、検証時は現在ファイルとの再照合に用いる。
|
|
9
|
+
* git commit SHA 等の非ファイル source は本ポート実装または usecase 側で source エントリ化する。
|
|
10
|
+
* 集約不変条件に関与しないファイル I/O 調停のため application に配置する。
|
|
11
|
+
*/
|
|
12
|
+
export interface SourceDigesterPort {
|
|
13
|
+
/** 相対/絶対パスを解決して現在の内容を読み、sha256 Digest を返す。 */
|
|
14
|
+
digestFile(path: string): Promise<Digest>;
|
|
15
|
+
}
|