Implement source lock and submission package baseline

This commit is contained in:
2026-05-16 02:51:00 +02:00
parent d73a73b455
commit c8ac42154c
18 changed files with 852 additions and 22 deletions

View File

@@ -42,6 +42,9 @@ The same CLI contracts are packaged by the container baseline. See
[docs/CONTAINER.md](docs/CONTAINER.md). The dependency-light local API wraps [docs/CONTAINER.md](docs/CONTAINER.md). The dependency-light local API wraps
those contracts for service and container operation; see those contracts for service and container operation; see
[docs/LOCAL-SERVICE-API.md](docs/LOCAL-SERVICE-API.md). [docs/LOCAL-SERVICE-API.md](docs/LOCAL-SERVICE-API.md).
New runs write a richer `sources.lock.json` and
`reports/submission-package.json` alongside the assessment package so reviewers
can inspect source, metadata, artifact, and boundary references.
The `sample-noop` extension exercises the guide-board contracts without invoking The `sample-noop` extension exercises the guide-board contracts without invoking
an external harness. `sdk-fixture` demonstrates the extension SDK contracts for an external harness. `sdk-fixture` demonstrates the extension SDK contracts for

View File

@@ -355,7 +355,9 @@ Stores run artifacts by reference and checksum:
The first implementation builds the assessment package artifact manifest from The first implementation builds the assessment package artifact manifest from
runner-emitted artifact refs and computes checksums for files inside the run runner-emitted artifact refs and computes checksums for files inside the run
directory. directory. New runs also write a source lock and a submission package manifest
that fingerprint reviewable run files and summarize runner or normalizer
metadata reported by extensions.
### Normalizer ### Normalizer
@@ -559,6 +561,18 @@ building complex runtime code.
- `artifact_policy` - `artifact_policy`
- `runtime_policy` - `runtime_policy`
### `SourceLock`
- `framework_refs`
- `extension_refs`
- `frameworks`
- `extensions`
- `mapping_sets`
- `profiles`
- `policy_refs`
- `authorities`
- `metadata_hooks`
### `RawArtifact` ### `RawArtifact`
- `id` - `id`
@@ -626,6 +640,19 @@ building complex runtime code.
- `certification_boundary` - `certification_boundary`
- `created_at` - `created_at`
### `SubmissionPackage`
- `run_id`
- `package_identity`
- `source_lock_ref`
- `source_lock`
- `reports`
- `normalized_outputs`
- `profile_snapshots`
- `artifact_manifest`
- `reported_metadata`
- `certification_boundary`
## Result Vocabulary ## Result Vocabulary
The evidence model should allow these statuses: The evidence model should allow these statuses:
@@ -714,6 +741,7 @@ runs/<run-id>/
reports/ reports/
report.md report.md
assessment-package.json assessment-package.json
submission-package.json
exports/ exports/
``` ```
@@ -787,7 +815,12 @@ Each run should lock:
- test suite IDs, - test suite IDs,
- mapping version, - mapping version,
- target profile snapshot, - target profile snapshot,
- waiver snapshot. - expectation and waiver refs.
The current source lock remains backward-compatible with the original
`framework_refs` and `extension_refs` fields while adding checksummed profiles,
mapping-set refs, optional policy refs, authority descriptors, and metadata
hooks for runners and normalizers.
## Implementation Sequence ## Implementation Sequence

View File

@@ -77,6 +77,7 @@ A completed CLI command prints a JSON result with:
- `run_dir`: output directory, - `run_dir`: output directory,
- `assessment_package`: JSON assessment package path, - `assessment_package`: JSON assessment package path,
- `report`: Markdown report path, - `report`: Markdown report path,
- `submission_package`: portable submission package manifest path,
- `retention_summary`: compact durable summary path. - `retention_summary`: compact durable summary path.
The output directory uses this contract: The output directory uses this contract:
@@ -84,15 +85,27 @@ The output directory uses this contract:
```text ```text
run.json run.json
plan.json plan.json
sources.lock.json
target-profile.snapshot.json
assessment-profile.snapshot.json
retention-summary.json retention-summary.json
normalized/evidence.json normalized/evidence.json
normalized/findings.json normalized/findings.json
normalized/mappings.json normalized/mappings.json
reports/assessment-package.json reports/assessment-package.json
reports/report.md reports/report.md
reports/submission-package.json
artifacts/ artifacts/
``` ```
`sources.lock.json` records the framework refs, extension versions, mapping
sets, profile snapshots, policy refs, authority refs, and extension metadata
hooks used for the run. `reports/submission-package.json` points at the
reviewable package files, includes checksums where files exist, carries the raw
artifact manifest, and repeats the certification boundary. It is a portable
handoff manifest for preparation evidence, not an authority-specific final
submission.
Use the retained run helpers for history: Use the retained run helpers for history:
```sh ```sh

View File

@@ -71,7 +71,12 @@ The key runtime fields are:
- `extension_type`: one of the supported archetypes from the architecture - `extension_type`: one of the supported archetypes from the architecture
blueprint. blueprint.
- `supported_frameworks`: framework IDs this extension can contribute evidence - `supported_frameworks`: framework IDs this extension can contribute evidence
for. for. Descriptor objects with `id`, `version`, `source_url`, and
`authority_ref` may be used when source metadata is available.
- `authorities`: authority IDs or descriptor objects with optional source URL,
version, license, and access notes.
- `metadata`: optional extension-level metadata such as adapter version or
source URL. The core preserves it in source locks and evidence metadata.
- `check_groups`: named groups that assessment profiles can select. - `check_groups`: named groups that assessment profiles can select.
- `preflight_runner`: optional runner ID used before selected check groups. - `preflight_runner`: optional runner ID used before selected check groups.
- `runner_entrypoints`: concrete runner declarations. - `runner_entrypoints`: concrete runner declarations.
@@ -141,6 +146,11 @@ Example:
"module_path": "src/open_cmis_tck/preflight.py", "module_path": "src/open_cmis_tck/preflight.py",
"callable": "run", "callable": "run",
"command": null, "command": null,
"metadata": {
"harness_id": "opencmis-tck",
"harness_version": "extension-detected-or-declared",
"source_url": "https://chemistry.apache.org/java/opencmis.html"
},
"description": "Checks whether the CMIS Browser Binding endpoint is reachable." "description": "Checks whether the CMIS Browser Binding endpoint is reachable."
} }
``` ```
@@ -272,11 +282,20 @@ Result fields:
- `observations`: human-readable observations. - `observations`: human-readable observations.
- `facts`: structured facts extracted by the runner. - `facts`: structured facts extracted by the runner.
- `artifact_refs`: references to raw artifacts written by the runner. - `artifact_refs`: references to raw artifacts written by the runner.
- `requirement_refs`: optional requirement refs discovered by the runner.
- `metadata`: optional generic metadata such as `harness_version`,
`test_suite_id`, `adapter_version`, `source_url`, or native result IDs.
Artifact refs must be paths relative to the run directory. After runner Artifact refs must be paths relative to the run directory. After runner
execution, the core fingerprints existing artifact refs into the assessment execution, the core fingerprints existing artifact refs into the assessment
package `artifact_manifest`. package `artifact_manifest`.
Runner metadata is merged with manifest entrypoint metadata and preserved under
evidence `facts.source_metadata`. The same metadata is also summarized in the
submission package manifest, which lets reviewers distinguish the extension
version from the harness or native test-suite version without adding
domain-specific fields to the core.
If a Python runner raises an exception, the core converts that failure into If a Python runner raises an exception, the core converts that failure into
`infrastructure_error` evidence so the assessment package remains complete. `infrastructure_error` evidence so the assessment package remains complete.
@@ -298,6 +317,9 @@ extension can add a normalizer descriptor:
"module_path": "normalizers/native_probe.py", "module_path": "normalizers/native_probe.py",
"callable": "normalize", "callable": "normalize",
"runner_ref": "native-probe", "runner_ref": "native-probe",
"metadata": {
"adapter_version": "0.1.0"
},
"description": "Converts native runner output into guide-board evidence." "description": "Converts native runner output into guide-board evidence."
} }
``` ```
@@ -340,6 +362,7 @@ The core merges the normalizer output over the runner result:
- `observations` are appended. - `observations` are appended.
- `facts` are merged. - `facts` are merged.
- `artifact_refs` and `requirement_refs` are deduplicated. - `artifact_refs` and `requirement_refs` are deduplicated.
- `metadata` is merged.
- `normalizer_refs` is recorded in evidence facts when any normalizer runs. - `normalizer_refs` is recorded in evidence facts when any normalizer runs.
If a normalizer raises an exception, the step becomes If a normalizer raises an exception, the step becomes
@@ -350,6 +373,25 @@ The bundled `extensions/sdk-fixture` extension is the copyable reference path
for profile schemas, a native-output runner, a normalizer, mappings, and fixture for profile schemas, a native-output runner, a normalizer, mappings, and fixture
profiles. profiles.
## Source Lock And Submission Package
Every new run writes `sources.lock.json` and
`reports/submission-package.json`. Extension authors should treat source
metadata as part of the evidence contract:
- declare extension, authority, framework, runner, and normalizer metadata in
`extension.json` when it is static;
- return runner or normalizer `metadata` when versions, native result IDs, or
test-suite IDs are detected at runtime;
- keep mapping sets under `mappings/` so the core can checksum them in the
source lock;
- keep restricted or licensed assets referenced by metadata or artifacts rather
than vendored into the core.
The submission package manifest is generic guide-board output. Authority-specific
final submissions, trademark assertions, or certification conclusions remain
extension-owned or reviewer-owned.
## Result Statuses ## Result Statuses
Initial statuses: Initial statuses:

View File

@@ -41,8 +41,38 @@
"type": "string", "type": "string",
"enum": ["candidate", "incubating", "active", "external", "deprecated"] "enum": ["candidate", "incubating", "active", "external", "deprecated"]
}, },
"supported_frameworks": { "type": "array", "items": { "type": "string" } }, "supported_frameworks": {
"authorities": { "type": "array", "items": { "type": "string" } }, "type": "array",
"items": {
"type": ["string", "object"],
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": { "type": "string" },
"version": { "type": ["string", "null"] },
"source_url": { "type": ["string", "null"] },
"authority_ref": { "type": ["string", "null"] },
"description": { "type": ["string", "null"] }
}
}
},
"authorities": {
"type": "array",
"items": {
"type": ["string", "object"],
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": { "type": "string" },
"name": { "type": ["string", "null"] },
"version": { "type": ["string", "null"] },
"source_url": { "type": ["string", "null"] },
"license": { "type": ["string", "null"] },
"access": { "type": ["string", "null"] }
}
}
},
"metadata": { "type": "object" },
"profile_schemas": { "profile_schemas": {
"type": "array", "type": "array",
"items": { "items": {
@@ -89,6 +119,7 @@
"module_path": { "type": ["string", "null"] }, "module_path": { "type": ["string", "null"] },
"callable": { "type": ["string", "null"] }, "callable": { "type": ["string", "null"] },
"command": { "type": ["array", "null"], "items": { "type": "string" } }, "command": { "type": ["array", "null"], "items": { "type": "string" } },
"metadata": { "type": "object" },
"description": { "type": ["string", "null"] } "description": { "type": ["string", "null"] }
} }
} }
@@ -105,6 +136,7 @@
"module_path": { "type": "string" }, "module_path": { "type": "string" },
"callable": { "type": "string" }, "callable": { "type": "string" },
"runner_ref": { "type": ["string", "null"] }, "runner_ref": { "type": ["string", "null"] },
"metadata": { "type": "object" },
"description": { "type": ["string", "null"] } "description": { "type": ["string", "null"] }
} }
} }

View File

@@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Guide Board Source Lock",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"schema_version",
"created_at",
"framework_refs",
"extension_refs",
"frameworks",
"extensions",
"mapping_sets",
"profiles",
"policy_refs",
"authorities",
"metadata_hooks"
],
"properties": {
"id": { "type": "string" },
"schema_version": { "type": "string" },
"created_at": { "type": "string" },
"framework_refs": { "type": "array", "items": { "type": "string" } },
"extension_refs": { "type": "array", "items": { "type": "string" } },
"frameworks": { "type": "array", "items": { "type": "object" } },
"extensions": { "type": "array", "items": { "type": "object" } },
"mapping_sets": { "type": "array", "items": { "type": "object" } },
"profiles": { "type": "object" },
"policy_refs": { "type": "object" },
"authorities": { "type": "array", "items": { "type": "object" } },
"metadata_hooks": { "type": "object" }
}
}

View File

@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Guide Board Submission Package Manifest",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"schema_version",
"run_id",
"created_at",
"package_identity",
"source_lock_ref",
"source_lock",
"reports",
"normalized_outputs",
"profile_snapshots",
"artifact_manifest",
"reported_metadata",
"certification_boundary"
],
"properties": {
"id": { "type": "string" },
"schema_version": { "type": "string" },
"run_id": { "type": "string" },
"created_at": { "type": "string" },
"package_identity": { "type": "object" },
"source_lock_ref": { "type": "string" },
"source_lock": { "type": "object" },
"reports": { "type": "array", "items": { "type": "object" } },
"normalized_outputs": { "type": "array", "items": { "type": "object" } },
"profile_snapshots": { "type": "array", "items": { "type": "object" } },
"artifact_manifest": { "type": "array", "items": { "type": "object" } },
"reported_metadata": { "type": "array", "items": { "type": "object" } },
"certification_boundary": { "type": "string" }
}
}

View File

@@ -8,6 +8,10 @@
"guide-board.sdk-fixture.v1" "guide-board.sdk-fixture.v1"
], ],
"authorities": [], "authorities": [],
"metadata": {
"adapter_version": "0.1.0",
"source_url": "https://example.invalid/guide-board/sdk-fixture"
},
"profile_schemas": [ "profile_schemas": [
"target-profile", "target-profile",
"assessment-profile", "assessment-profile",
@@ -44,6 +48,12 @@
"module_path": "runners/native_probe.py", "module_path": "runners/native_probe.py",
"callable": "run", "callable": "run",
"command": null, "command": null,
"metadata": {
"harness_id": "sdk-fixture-native-probe",
"harness_version": "1.0.0",
"test_suite_id": "sdk-fixture-suite-v1",
"source_url": "https://example.invalid/guide-board/sdk-fixture/native-probe"
},
"description": "Writes a tiny native result artifact for the SDK fixture normalizer." "description": "Writes a tiny native result artifact for the SDK fixture normalizer."
} }
], ],
@@ -54,6 +64,10 @@
"module_path": "normalizers/native_probe.py", "module_path": "normalizers/native_probe.py",
"callable": "normalize", "callable": "normalize",
"runner_ref": "native-probe", "runner_ref": "native-probe",
"metadata": {
"adapter_version": "0.1.0",
"source_url": "https://example.invalid/guide-board/sdk-fixture/native-normalizer"
},
"description": "Converts the SDK fixture native result artifact into guide-board evidence." "description": "Converts the SDK fixture native result artifact into guide-board evidence."
} }
], ],

View File

@@ -25,4 +25,8 @@ def normalize(context: dict) -> dict:
artifact_ref artifact_ref
], ],
"requirement_refs": native_result.get("requirement_refs", []), "requirement_refs": native_result.get("requirement_refs", []),
"metadata": {
"normalizer_id": "native-probe-normalizer",
"native_result_id": "sdk-fixture-native-result"
},
} }

View File

@@ -33,4 +33,8 @@ def run(context: dict) -> dict:
"artifact_refs": [ "artifact_refs": [
artifact_ref artifact_ref
], ],
"metadata": {
"native_result_id": "sdk-fixture-native-result",
"test_suite_id": "sdk-fixture-suite-v1"
},
} }

View File

@@ -46,6 +46,107 @@ def build_artifact_manifest(
return artifacts return artifacts
def build_submission_manifest(
run_dir: Path,
run_metadata: dict[str, Any],
plan: dict[str, Any],
evidence: list[dict[str, Any]],
assessment_package: dict[str, Any],
) -> dict[str, Any]:
"""Build a portable manifest for the files that make up a review package."""
source_lock = plan["source_lock"]
manifest = {
"id": f"submission-package:{run_metadata['id']}",
"schema_version": "guide-board.submission-package.v1",
"run_id": run_metadata["id"],
"created_at": datetime.now(timezone.utc).isoformat(),
"package_identity": {
"target_profile_ref": run_metadata["target_profile_ref"],
"assessment_profile_ref": run_metadata["assessment_profile_ref"],
"framework_refs": source_lock.get("framework_refs", []),
"extension_refs": source_lock.get("extension_refs", []),
},
"source_lock_ref": "sources.lock.json",
"source_lock": {
"id": source_lock.get("id"),
"schema_version": source_lock.get("schema_version"),
"checksum": _file_entry(run_dir, "sources.lock.json").get("checksum"),
"framework_refs": source_lock.get("framework_refs", []),
"extension_refs": source_lock.get("extension_refs", []),
},
"reports": _existing_file_entries(
run_dir,
[
("assessment-package", "reports/assessment-package.json"),
("markdown-report", "reports/report.md"),
],
),
"normalized_outputs": _existing_file_entries(
run_dir,
[
("evidence", "normalized/evidence.json"),
("findings", "normalized/findings.json"),
("mappings", "normalized/mappings.json"),
],
),
"profile_snapshots": _existing_file_entries(
run_dir,
[
("target-profile", "target-profile.snapshot.json"),
("assessment-profile", "assessment-profile.snapshot.json"),
],
),
"artifact_manifest": assessment_package.get("artifact_manifest", []),
"reported_metadata": _reported_metadata(evidence),
"certification_boundary": assessment_package["certification_boundary"],
}
assert_valid(manifest, "submission-package")
return manifest
def _existing_file_entries(run_dir: Path, refs: list[tuple[str, str]]) -> list[dict[str, Any]]:
entries = []
for entry_id, ref in refs:
entry = _file_entry(run_dir, ref)
if entry:
entry["id"] = entry_id
entries.append(entry)
return entries
def _file_entry(run_dir: Path, ref: str) -> dict[str, Any]:
path = (run_dir / ref).resolve()
try:
path.relative_to(run_dir.resolve())
except ValueError:
return {}
if not path.is_file():
return {}
return {
"path": ref,
"media_type": _media_type(path),
"checksum": f"sha256:{_sha256(path)}",
"size_bytes": path.stat().st_size,
}
def _reported_metadata(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
records = []
for item in evidence:
metadata = item.get("facts", {}).get("source_metadata")
if not isinstance(metadata, dict) or not metadata:
continue
records.append(
{
"evidence_ref": item["id"],
"check_id": item["check_id"],
"extension_id": item["extension_id"],
"metadata": metadata,
}
)
return records
def _sha256(path: Path) -> str: def _sha256(path: Path) -> str:
digest = hashlib.sha256() digest = hashlib.sha256()
with path.open("rb") as handle: with path.open("rb") as handle:

View File

@@ -7,8 +7,8 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from guide_board.artifacts import build_artifact_manifest from guide_board.artifacts import build_artifact_manifest, build_submission_manifest
from guide_board.io import write_json from guide_board.io import load_json, write_json
from guide_board.mapping import build_mapping_records, summarize_mappings from guide_board.mapping import build_mapping_records, summarize_mappings
from guide_board.normalizers import normalize_step_result from guide_board.normalizers import normalize_step_result
from guide_board.planning import build_run_plan from guide_board.planning import build_run_plan
@@ -83,6 +83,7 @@ def run_assessment(
"run_dir": str(run_dir), "run_dir": str(run_dir),
"assessment_package": str(run_dir / "reports" / "assessment-package.json"), "assessment_package": str(run_dir / "reports" / "assessment-package.json"),
"report": str(run_dir / "reports" / "report.md"), "report": str(run_dir / "reports" / "report.md"),
"submission_package": str(run_dir / "reports" / "submission-package.json"),
"retention_summary": str(run_dir / "retention-summary.json"), "retention_summary": str(run_dir / "retention-summary.json"),
} }
@@ -155,6 +156,14 @@ def _evidence_for_step(
runner_ref = step.get("runner_ref") runner_ref = step.get("runner_ref")
runner_result = run_step(root, run_dir, run_id, plan, step) runner_result = run_step(root, run_dir, run_id, plan, step)
runner_result = normalize_step_result(root, run_dir, run_id, plan, step, runner_result) runner_result = normalize_step_result(root, run_dir, run_id, plan, step, runner_result)
facts = {
"step_kind": step["kind"],
"runner_ref": runner_ref,
**runner_result["facts"],
}
source_metadata = _source_metadata_for_step(root, plan, step, runner_result)
if source_metadata:
facts["source_metadata"] = source_metadata
return { return {
"id": f"evidence:{step['id']}", "id": f"evidence:{step['id']}",
@@ -164,11 +173,7 @@ def _evidence_for_step(
"subject_ref": plan["target_profile_snapshot"]["id"], "subject_ref": plan["target_profile_snapshot"]["id"],
"result": runner_result["result"], "result": runner_result["result"],
"observations": runner_result["observations"], "observations": runner_result["observations"],
"facts": { "facts": facts,
"step_kind": step["kind"],
"runner_ref": runner_ref,
**runner_result["facts"],
},
"requirement_refs": _requirement_refs(plan, step, runner_result), "requirement_refs": _requirement_refs(plan, step, runner_result),
"artifact_refs": runner_result["artifact_refs"], "artifact_refs": runner_result["artifact_refs"],
"started_at": now, "started_at": now,
@@ -198,6 +203,95 @@ def _runner_requirement_refs(runner_result: dict[str, Any] | None) -> list[str]:
return [ref for ref in refs if isinstance(ref, str)] return [ref for ref in refs if isinstance(ref, str)]
def _source_metadata_for_step(
root: Path,
plan: dict[str, Any],
step: dict[str, Any],
runner_result: dict[str, Any],
) -> dict[str, Any]:
runner_ref = step.get("runner_ref")
extension = _extension_snapshot(plan, step["extension_id"])
extension_path = _snapshot_path(root, extension)
manifest = load_json(extension_path / "extension.json")
metadata: dict[str, Any] = {
"extension": {
"id": step["extension_id"],
"version": extension.get("version"),
"metadata": _object_or_empty(manifest.get("metadata")),
}
}
if runner_ref:
entrypoint = _runner_entrypoint(manifest, runner_ref)
metadata["runner"] = {
"id": runner_ref,
"kind": entrypoint.get("kind"),
"metadata": _object_or_empty(entrypoint.get("metadata")),
}
applied_normalizers = runner_result.get("facts", {}).get("normalizer_refs", [])
normalizers = []
if isinstance(applied_normalizers, list):
normalizer_ids = {item for item in applied_normalizers if isinstance(item, str)}
for normalizer in manifest.get("normalizers", []):
if isinstance(normalizer, dict) and normalizer.get("id") in normalizer_ids:
normalizers.append(
{
"id": normalizer["id"],
"kind": normalizer.get("kind"),
"runner_ref": normalizer.get("runner_ref"),
"metadata": _object_or_empty(normalizer.get("metadata")),
}
)
if normalizers:
metadata["normalizers"] = normalizers
reported = _object_or_empty(runner_result.get("metadata"))
if reported:
metadata["reported"] = reported
return _drop_empty_metadata(metadata)
def _extension_snapshot(plan: dict[str, Any], extension_id: str) -> dict[str, Any]:
for extension in plan["extension_snapshots"]:
if extension["id"] == extension_id:
return extension
return {}
def _snapshot_path(root: Path, extension: dict[str, Any]) -> Path:
path = Path(extension["path"])
return path if path.is_absolute() else root / path
def _runner_entrypoint(manifest: dict[str, Any], runner_ref: str) -> dict[str, Any]:
for entrypoint in manifest.get("runner_entrypoints", []):
if entrypoint.get("id") == runner_ref:
return entrypoint
return {}
def _object_or_empty(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _drop_empty_metadata(value: dict[str, Any]) -> dict[str, Any]:
compact = {}
for key, child in value.items():
if isinstance(child, dict):
child = _drop_empty_metadata(child)
if isinstance(child, list):
child = [
_drop_empty_metadata(item) if isinstance(item, dict) else item
for item in child
]
child = [item for item in child if item]
if child:
compact[key] = child
return compact
def _dedupe(values: list[str]) -> list[str]: def _dedupe(values: list[str]) -> list[str]:
seen = set() seen = set()
deduped = [] deduped = []
@@ -340,6 +434,14 @@ def _write_run_directory(
_markdown_report(run_metadata, assessment_package), _markdown_report(run_metadata, assessment_package),
encoding="utf-8", encoding="utf-8",
) )
submission_manifest = build_submission_manifest(
run_dir,
run_metadata,
plan,
evidence,
assessment_package,
)
write_json(run_dir / "reports" / "submission-package.json", submission_manifest)
def _markdown_report(run_metadata: dict[str, Any], package: dict[str, Any]) -> str: def _markdown_report(run_metadata: dict[str, Any], package: dict[str, Any]) -> str:

View File

@@ -127,6 +127,7 @@ def _run_normalizer(
}, },
"artifact_refs": runner_result.get("artifact_refs", []), "artifact_refs": runner_result.get("artifact_refs", []),
"requirement_refs": runner_result.get("requirement_refs", []), "requirement_refs": runner_result.get("requirement_refs", []),
"metadata": runner_result.get("metadata", {}),
} }
if not isinstance(result, dict): if not isinstance(result, dict):
@@ -160,6 +161,12 @@ def _merge_result(
_string_list(base.get("requirement_refs", [])) _string_list(base.get("requirement_refs", []))
+ _string_list(update.get("requirement_refs", [])) + _string_list(update.get("requirement_refs", []))
) )
if "metadata" in update:
metadata = dict(base.get("metadata", {}))
update_metadata = update.get("metadata", {})
if isinstance(update_metadata, dict):
metadata.update(update_metadata)
merged["metadata"] = metadata
return _coerce_result(merged) return _coerce_result(merged)
@@ -173,6 +180,7 @@ def _coerce_result(value: dict[str, Any]) -> dict[str, Any]:
"facts": facts, "facts": facts,
"artifact_refs": _string_list(value.get("artifact_refs", [])), "artifact_refs": _string_list(value.get("artifact_refs", [])),
"requirement_refs": _string_list(value.get("requirement_refs", [])), "requirement_refs": _string_list(value.get("requirement_refs", [])),
"metadata": _object_or_empty(value.get("metadata")),
} }
@@ -189,6 +197,10 @@ def _string_list(value: Any) -> list[str]:
return [item for item in value if isinstance(item, str)] return [item for item in value if isinstance(item, str)]
def _object_or_empty(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _dedupe(values: list[str]) -> list[str]: def _dedupe(values: list[str]) -> list[str]:
seen = set() seen = set()
deduped = [] deduped = []

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import hashlib
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -96,6 +97,16 @@ def build_run_plan(
} }
) )
source_lock = _build_source_lock(
root,
target_path,
assessment_path,
target,
assessment,
[extensions[extension_id] for extension_id in selected_extensions],
)
assert_valid(source_lock, "source-lock")
plan = { plan = {
"id": f"plan-{_timestamp()}", "id": f"plan-{_timestamp()}",
"assessment_profile_snapshot": assessment, "assessment_profile_snapshot": assessment,
@@ -109,10 +120,7 @@ def build_run_plan(
} }
for extension_id in selected_extensions for extension_id in selected_extensions
], ],
"source_lock": { "source_lock": source_lock,
"framework_refs": assessment["framework_refs"],
"extension_refs": selected_extensions,
},
"profile_paths": { "profile_paths": {
"target_profile_path": str(target_path.resolve()), "target_profile_path": str(target_path.resolve()),
"assessment_profile_path": str(assessment_path.resolve()), "assessment_profile_path": str(assessment_path.resolve()),
@@ -208,6 +216,270 @@ def _load_extension_profile_schema(
return load_json(schema_path) return load_json(schema_path)
def _build_source_lock(
root: Path,
target_path: Path,
assessment_path: Path,
target: dict[str, Any],
assessment: dict[str, Any],
extensions: list[Extension],
) -> dict[str, Any]:
framework_refs = assessment["framework_refs"]
extension_refs = [extension.id for extension in extensions]
return {
"id": f"source-lock:{assessment['id']}:{target['id']}",
"schema_version": "guide-board.source-lock.v1",
"created_at": _now(),
"framework_refs": framework_refs,
"extension_refs": extension_refs,
"frameworks": _framework_records(framework_refs, extensions),
"extensions": [_extension_source_record(root, extension) for extension in extensions],
"mapping_sets": _mapping_source_records(root, extensions),
"profiles": {
"target": _file_source_record(
"target-profile",
target["id"],
target_path,
"target-profile.snapshot.json",
),
"assessment": _file_source_record(
"assessment-profile",
assessment["id"],
assessment_path,
"assessment-profile.snapshot.json",
),
},
"policy_refs": {
"expectations": _optional_policy_source_record(
root,
assessment_path,
assessment.get("expectations_ref"),
"expectation-set",
),
"waivers": _optional_policy_source_record(
root,
assessment_path,
assessment.get("waivers_ref"),
"waiver-set",
),
},
"authorities": _authority_source_records(extensions),
"metadata_hooks": {
"runner_entrypoints": _entrypoint_metadata_records(extensions),
"normalizers": _normalizer_metadata_records(extensions),
},
}
def _framework_records(
framework_refs: list[str],
extensions: list[Extension],
) -> list[dict[str, Any]]:
records = []
for framework_ref in framework_refs:
declaring_extensions = [
extension.id
for extension in extensions
if framework_ref in _manifest_framework_ids(extension.manifest)
]
records.append(
{
"id": framework_ref,
"version": _version_hint(framework_ref),
"declared_by_extensions": declaring_extensions,
}
)
return records
def _extension_source_record(root: Path, extension: Extension) -> dict[str, Any]:
manifest_path = extension.path / "extension.json"
return {
"id": extension.id,
"version": extension.manifest["version"],
"path": _extension_path_ref(root, extension.path),
"source": extension.source,
"manifest_path": _display_path(root, manifest_path),
"manifest_checksum": _checksum_if_file(manifest_path),
"supported_frameworks": _manifest_framework_ids(extension.manifest),
"authorities": _authority_ids(extension.manifest.get("authorities", [])),
"certification_boundary": extension.manifest["certification_boundary"],
"metadata": _object_or_empty(extension.manifest.get("metadata")),
}
def _mapping_source_records(root: Path, extensions: list[Extension]) -> list[dict[str, Any]]:
records = []
for extension in extensions:
for mapping_id in extension.manifest.get("mappings", []):
if not isinstance(mapping_id, str):
continue
mapping_path = extension.path / "mappings" / f"{mapping_id}.json"
record = {
"id": mapping_id,
"extension_id": extension.id,
"path": _display_path(root, mapping_path),
"exists": mapping_path.is_file(),
"checksum": _checksum_if_file(mapping_path),
"framework_refs": [],
}
if mapping_path.is_file():
mapping_set = load_json(mapping_path)
record["framework_refs"] = _string_list(mapping_set.get("framework_refs", []))
records.append(record)
return records
def _file_source_record(
kind: str,
profile_id: str,
path: Path,
snapshot_ref: str,
) -> dict[str, Any]:
return {
"kind": kind,
"id": profile_id,
"path": str(path.resolve()),
"checksum": _checksum_if_file(path),
"snapshot_ref": snapshot_ref,
}
def _optional_policy_source_record(
root: Path,
assessment_path: Path,
ref: Any,
kind: str,
) -> dict[str, Any] | None:
if not isinstance(ref, str) or not ref:
return None
path = _resolve_assessment_ref(root, assessment_path, ref)
return {
"kind": kind,
"ref": ref,
"path": str(path.resolve()),
"exists": path.is_file(),
"checksum": _checksum_if_file(path),
}
def _authority_source_records(extensions: list[Extension]) -> list[dict[str, Any]]:
records = []
for extension in extensions:
for authority in extension.manifest.get("authorities", []):
if isinstance(authority, str):
records.append({"id": authority, "extension_id": extension.id})
elif isinstance(authority, dict):
record = {
"id": authority.get("id"),
"extension_id": extension.id,
}
for key in ("name", "version", "source_url", "license", "access"):
if key in authority:
record[key] = authority[key]
records.append(record)
return [record for record in records if isinstance(record.get("id"), str)]
def _entrypoint_metadata_records(extensions: list[Extension]) -> list[dict[str, Any]]:
records = []
for extension in extensions:
for entrypoint in extension.manifest.get("runner_entrypoints", []):
if not isinstance(entrypoint, dict):
continue
records.append(
{
"extension_id": extension.id,
"id": entrypoint.get("id"),
"kind": entrypoint.get("kind"),
"metadata": _object_or_empty(entrypoint.get("metadata")),
}
)
return records
def _normalizer_metadata_records(extensions: list[Extension]) -> list[dict[str, Any]]:
records = []
for extension in extensions:
for normalizer in extension.manifest.get("normalizers", []):
if not isinstance(normalizer, dict):
continue
records.append(
{
"extension_id": extension.id,
"id": normalizer.get("id"),
"kind": normalizer.get("kind"),
"runner_ref": normalizer.get("runner_ref"),
"metadata": _object_or_empty(normalizer.get("metadata")),
}
)
return records
def _manifest_framework_ids(manifest: dict[str, Any]) -> list[str]:
values = []
for framework in manifest.get("supported_frameworks", []):
if isinstance(framework, str):
values.append(framework)
elif isinstance(framework, dict) and isinstance(framework.get("id"), str):
values.append(framework["id"])
return values
def _authority_ids(authorities: list[Any]) -> list[str]:
values = []
for authority in authorities:
if isinstance(authority, str):
values.append(authority)
elif isinstance(authority, dict) and isinstance(authority.get("id"), str):
values.append(authority["id"])
return values
def _resolve_assessment_ref(root: Path, assessment_path: Path, ref: str) -> Path:
ref_path = Path(ref)
if ref_path.is_absolute():
return ref_path
root_relative = root / ref_path
if root_relative.exists():
return root_relative
return assessment_path.resolve().parent / ref_path
def _checksum_if_file(path: Path) -> str | None:
if not path.is_file():
return None
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return f"sha256:{digest.hexdigest()}"
def _version_hint(ref: str) -> str | None:
for part in reversed(ref.replace("-", ".").split(".")):
if len(part) > 1 and part[0].lower() == "v" and any(char.isdigit() for char in part[1:]):
return part
return None
def _object_or_empty(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _string_list(value: Any) -> list[str]:
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, str)]
def _display_path(root: Path, path: Path) -> str:
try:
return str(path.resolve().relative_to(root.resolve()))
except ValueError:
return str(path.resolve())
def _extension_path_ref(root: Path, path: Path) -> str: def _extension_path_ref(root: Path, path: Path) -> str:
try: try:
return str(path.resolve().relative_to(root.resolve())) return str(path.resolve().relative_to(root.resolve()))
@@ -215,5 +487,9 @@ def _extension_path_ref(root: Path, path: Path) -> str:
return str(path.resolve()) return str(path.resolve())
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _timestamp() -> str: def _timestamp() -> str:
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

View File

@@ -45,6 +45,7 @@ def build_retention_summary(
"report_refs": [ "report_refs": [
"reports/assessment-package.json", "reports/assessment-package.json",
"reports/report.md", "reports/report.md",
"reports/submission-package.json",
], ],
"artifact_retention": { "artifact_retention": {
"policy": plan["assessment_profile_snapshot"].get("retention_policy", {}), "policy": plan["assessment_profile_snapshot"].get("retention_policy", {}),

View File

@@ -45,6 +45,8 @@ def run_step(
"runner_kind": "external", "runner_kind": "external",
}, },
"artifact_refs": [], "artifact_refs": [],
"requirement_refs": [],
"metadata": _object_or_empty(entrypoint.get("metadata")),
} }
if entrypoint["kind"] == "command": if entrypoint["kind"] == "command":
return _run_command(root, run_dir, run_id, plan, step, extension_path, entrypoint) return _run_command(root, run_dir, run_id, plan, step, extension_path, entrypoint)
@@ -63,6 +65,8 @@ def _no_runner_result(step: dict[str, Any]) -> dict[str, Any]:
"runner_kind": None, "runner_kind": None,
}, },
"artifact_refs": [], "artifact_refs": [],
"requirement_refs": [],
"metadata": {},
} }
@@ -118,6 +122,8 @@ def _run_python_module(
"error_type": type(exc).__name__, "error_type": type(exc).__name__,
}, },
"artifact_refs": [], "artifact_refs": [],
"requirement_refs": [],
"metadata": _object_or_empty(entrypoint.get("metadata")),
} }
if not isinstance(result, dict): if not isinstance(result, dict):
raise ValidationError(f"{entrypoint['id']}: runner must return an object") raise ValidationError(f"{entrypoint['id']}: runner must return an object")
@@ -126,6 +132,8 @@ def _run_python_module(
"observations": result.get("observations", []), "observations": result.get("observations", []),
"facts": result.get("facts", {}), "facts": result.get("facts", {}),
"artifact_refs": result.get("artifact_refs", []), "artifact_refs": result.get("artifact_refs", []),
"requirement_refs": result.get("requirement_refs", []),
"metadata": _merge_metadata(entrypoint.get("metadata"), result.get("metadata")),
} }
@@ -192,6 +200,8 @@ def _run_command(
"command": command, "command": command,
}, },
"artifact_refs": [str(context_path.relative_to(run_dir))], "artifact_refs": [str(context_path.relative_to(run_dir))],
"requirement_refs": [],
"metadata": _object_or_empty(entrypoint.get("metadata")),
} }
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return { return {
@@ -206,6 +216,8 @@ def _run_command(
"command": command, "command": command,
}, },
"artifact_refs": [str(context_path.relative_to(run_dir))], "artifact_refs": [str(context_path.relative_to(run_dir))],
"requirement_refs": [],
"metadata": _object_or_empty(entrypoint.get("metadata")),
} }
parsed = _parse_runner_stdout(completed.stdout) parsed = _parse_runner_stdout(completed.stdout)
@@ -225,6 +237,8 @@ def _run_command(
"command": command, "command": command,
}, },
"artifact_refs": [str(context_path.relative_to(run_dir))], "artifact_refs": [str(context_path.relative_to(run_dir))],
"requirement_refs": [],
"metadata": _object_or_empty(entrypoint.get("metadata")),
} }
facts = parsed.get("facts", {}) facts = parsed.get("facts", {})
@@ -245,6 +259,9 @@ def _run_command(
if not isinstance(artifact_refs, list): if not isinstance(artifact_refs, list):
artifact_refs = [] artifact_refs = []
artifact_refs.append(str(context_path.relative_to(run_dir))) artifact_refs.append(str(context_path.relative_to(run_dir)))
requirement_refs = parsed.get("requirement_refs", [])
if not isinstance(requirement_refs, list):
requirement_refs = []
result = parsed.get("result", "unknown") result = parsed.get("result", "unknown")
if completed.returncode != 0 and result in {"pass", "warning", "manual", "skipped"}: if completed.returncode != 0 and result in {"pass", "warning", "manual", "skipped"}:
@@ -258,6 +275,8 @@ def _run_command(
"observations": observations, "observations": observations,
"facts": facts, "facts": facts,
"artifact_refs": artifact_refs, "artifact_refs": artifact_refs,
"requirement_refs": requirement_refs,
"metadata": _merge_metadata(entrypoint.get("metadata"), parsed.get("metadata")),
} }
@@ -328,5 +347,17 @@ def _parse_runner_stdout(stdout: str) -> dict[str, Any] | None:
return parsed return parsed
def _merge_metadata(*values: Any) -> dict[str, Any]:
merged: dict[str, Any] = {}
for value in values:
if isinstance(value, dict):
merged.update(value)
return merged
def _object_or_empty(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _safe_id(value: str) -> str: def _safe_id(value: str) -> str:
return "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in value) return "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in value)

View File

@@ -75,6 +75,17 @@ class CoreArchitectureTests(unittest.TestCase):
plan["ordered_steps"][1]["requirement_refs"], plan["ordered_steps"][1]["requirement_refs"],
["guide-board.sample-readiness.v0.profile-shape"], ["guide-board.sample-readiness.v0.profile-shape"],
) )
assert_valid(plan["source_lock"], "source-lock")
self.assertEqual(plan["source_lock"]["schema_version"], "guide-board.source-lock.v1")
self.assertEqual(plan["source_lock"]["framework_refs"], ["guide-board.sample-readiness.v0"])
self.assertEqual(plan["source_lock"]["extension_refs"], ["sample-noop"])
self.assertEqual(
plan["source_lock"]["profiles"]["target"]["snapshot_ref"],
"target-profile.snapshot.json",
)
self.assertTrue(plan["source_lock"]["profiles"]["target"]["checksum"].startswith("sha256:"))
self.assertEqual(plan["source_lock"]["mapping_sets"][0]["id"], "sample-readiness-map")
self.assertTrue(plan["source_lock"]["mapping_sets"][0]["checksum"].startswith("sha256:"))
def test_runs_external_extension_from_separate_repo(self) -> None: def test_runs_external_extension_from_separate_repo(self) -> None:
with TemporaryDirectory() as temporary_directory: with TemporaryDirectory() as temporary_directory:
@@ -237,8 +248,37 @@ class CoreArchitectureTests(unittest.TestCase):
check_evidence["artifact_refs"], check_evidence["artifact_refs"],
["artifacts/sdk-fixture/native-result.json"], ["artifacts/sdk-fixture/native-result.json"],
) )
self.assertEqual(
check_evidence["facts"]["source_metadata"]["runner"]["metadata"]["harness_version"],
"1.0.0",
)
self.assertEqual(
check_evidence["facts"]["source_metadata"]["reported"]["native_result_id"],
"sdk-fixture-native-result",
)
self.assertEqual(mappings[0]["target_id"], "normalizer-plugin") self.assertEqual(mappings[0]["target_id"], "normalizer-plugin")
self.assertEqual(assessment_package["summary"], {"pass": 1, "skipped": 1}) self.assertEqual(assessment_package["summary"], {"pass": 1, "skipped": 1})
self.assertEqual(
assessment_package["source_lock"]["metadata_hooks"]["runner_entrypoints"][0][
"metadata"
]["harness_id"],
"sdk-fixture-native-probe",
)
submission_package = json.loads(
(run_dir / "reports" / "submission-package.json").read_text(encoding="utf-8")
)
assert_valid(submission_package, "submission-package")
self.assertEqual(submission_package["source_lock"]["id"], "source-lock:sdk-fixture-assessment:sdk-fixture-target")
self.assertEqual(
submission_package["reported_metadata"][1]["metadata"]["reported"][
"native_result_id"
],
"sdk-fixture-native-result",
)
self.assertEqual(
submission_package["artifact_manifest"][0]["checksum"],
assessment_package["artifact_manifest"][0]["checksum"],
)
def test_runs_sample_noop_assessment(self) -> None: def test_runs_sample_noop_assessment(self) -> None:
with TemporaryDirectory() as temporary_directory: with TemporaryDirectory() as temporary_directory:
@@ -256,6 +296,7 @@ class CoreArchitectureTests(unittest.TestCase):
self.assertTrue((run_dir / "normalized" / "evidence.json").exists()) self.assertTrue((run_dir / "normalized" / "evidence.json").exists())
self.assertTrue((run_dir / "reports" / "assessment-package.json").exists()) self.assertTrue((run_dir / "reports" / "assessment-package.json").exists())
self.assertTrue((run_dir / "reports" / "report.md").exists()) self.assertTrue((run_dir / "reports" / "report.md").exists())
self.assertTrue((run_dir / "reports" / "submission-package.json").exists())
retention = json.loads( retention = json.loads(
(run_dir / "retention-summary.json").read_text(encoding="utf-8") (run_dir / "retention-summary.json").read_text(encoding="utf-8")
) )
@@ -263,12 +304,26 @@ class CoreArchitectureTests(unittest.TestCase):
result["retention_summary"], result["retention_summary"],
str(run_dir / "retention-summary.json"), str(run_dir / "retention-summary.json"),
) )
self.assertEqual(
result["submission_package"],
str(run_dir / "reports" / "submission-package.json"),
)
self.assertEqual(retention["summary"]["status"], "completed") self.assertEqual(retention["summary"]["status"], "completed")
self.assertEqual(retention["summary"]["artifact_count"], 0) self.assertEqual(retention["summary"]["artifact_count"], 0)
self.assertIn("reports/submission-package.json", retention["report_refs"])
self.assertEqual( self.assertEqual(
retention["artifact_retention"]["policy"], retention["artifact_retention"]["policy"],
{"raw_artifact_days": 0, "summary_days": 365}, {"raw_artifact_days": 0, "summary_days": 365},
) )
submission = json.loads(
(run_dir / "reports" / "submission-package.json").read_text(encoding="utf-8")
)
assert_valid(submission, "submission-package")
self.assertEqual(submission["package_identity"]["target_profile_ref"], "sample-repository")
self.assertEqual(
[entry["path"] for entry in submission["reports"]],
["reports/assessment-package.json", "reports/report.md"],
)
self.assertEqual( self.assertEqual(
[run["run_id"] for run in list_retained_runs(Path(temporary_directory))], [run["run_id"] for run in list_retained_runs(Path(temporary_directory))],
[result["run_id"]], [result["run_id"]],

View File

@@ -4,12 +4,12 @@ type: workplan
title: "Source Lock And Submission Package Baseline" title: "Source Lock And Submission Package Baseline"
repo: guide-board repo: guide-board
domain: markitect domain: markitect
status: active status: completed
owner: codex owner: codex
planning_priority: high planning_priority: high
planning_order: 4 planning_order: 4
created: "2026-05-15" created: "2026-05-15"
updated: "2026-05-15" updated: "2026-05-16"
state_hub_workstream_id: "6dd2832b-d1d9-43bc-ad5c-d16f399930dc" state_hub_workstream_id: "6dd2832b-d1d9-43bc-ad5c-d16f399930dc"
--- ---
@@ -41,7 +41,7 @@ submission rules, and licensed or restricted assets remain extension-owned.
```task ```task
id: GUIDE-BOARD-WP-0004-T001 id: GUIDE-BOARD-WP-0004-T001
status: todo status: done
priority: high priority: high
state_hub_task_id: "d5a7a18f-941b-47b8-9992-2cb54bc5ad06" state_hub_task_id: "d5a7a18f-941b-47b8-9992-2cb54bc5ad06"
``` ```
@@ -55,11 +55,20 @@ Acceptance:
- Keep the schema backward-compatible with existing retained runs. - Keep the schema backward-compatible with existing retained runs.
- Add tests for source lock shape and retained run compatibility. - Add tests for source lock shape and retained run compatibility.
Progress:
- Added `docs/schemas/source-lock.schema.json`.
- Expanded run-plan source locks with framework, extension, mapping-set,
profile snapshot, policy-ref, authority, and metadata-hook records.
- Preserved the original `framework_refs` and `extension_refs` fields for
retained-run compatibility.
- Added tests for source-lock shape and older retained summary compatibility.
## D4.2 - Harness And Extension Metadata Hooks ## D4.2 - Harness And Extension Metadata Hooks
```task ```task
id: GUIDE-BOARD-WP-0004-T002 id: GUIDE-BOARD-WP-0004-T002
status: todo status: done
priority: high priority: high
state_hub_task_id: "7abd5a66-5784-41b9-a361-6572290923cc" state_hub_task_id: "7abd5a66-5784-41b9-a361-6572290923cc"
``` ```
@@ -75,11 +84,20 @@ Acceptance:
provide this metadata yet. provide this metadata yet.
- Cover the SDK fixture and at least one no-metadata extension path in tests. - Cover the SDK fixture and at least one no-metadata extension path in tests.
Progress:
- Added optional manifest metadata for extensions, authorities, frameworks,
runner entrypoints, and normalizers.
- Preserved runner and normalizer returned `metadata` and requirement refs.
- Recorded merged metadata under evidence `facts.source_metadata`.
- Updated `sdk-fixture` to exercise harness, test-suite, adapter, source URL,
and native result metadata while keeping `sample-noop` as a no-metadata path.
## D4.3 - Submission Package Manifest ## D4.3 - Submission Package Manifest
```task ```task
id: GUIDE-BOARD-WP-0004-T003 id: GUIDE-BOARD-WP-0004-T003
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "c54273d6-1fc2-4444-92cf-74f2a5e614ec" state_hub_task_id: "c54273d6-1fc2-4444-92cf-74f2a5e614ec"
``` ```
@@ -95,11 +113,21 @@ Acceptance:
packs. packs.
- Document how this differs from an authority-specific final submission. - Document how this differs from an authority-specific final submission.
Progress:
- Added `docs/schemas/submission-package.schema.json`.
- Wrote `reports/submission-package.json` for each run.
- Included package identity, source lock checksum, report paths, normalized
output paths, profile snapshots, artifact manifest entries, reported
metadata, and the certification boundary.
- Exposed the submission manifest path in CLI/service run results and retained
report refs.
## D4.4 - Documentation And Acceptance Tests ## D4.4 - Documentation And Acceptance Tests
```task ```task
id: GUIDE-BOARD-WP-0004-T004 id: GUIDE-BOARD-WP-0004-T004
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "ad37baeb-973c-4399-96d0-c9cb7fc6b761" state_hub_task_id: "ad37baeb-973c-4399-96d0-c9cb7fc6b761"
``` ```
@@ -113,6 +141,15 @@ Acceptance:
- Include compatibility notes for older retained runs. - Include compatibility notes for older retained runs.
- Keep the output paths aligned with existing CLI and service result retrieval. - Keep the output paths aligned with existing CLI and service result retrieval.
Progress:
- Updated assessment operations, extension SDK, architecture blueprint, and
README references.
- Added focused unit assertions for the sample and SDK fixture assessment
outputs.
- Kept retained run listing compatible with older `retention-summary.json`
files that do not reference a submission manifest.
## Definition Of Done ## Definition Of Done
- Every new run writes a richer source lock and submission package manifest. - Every new run writes a richer source lock and submission package manifest.