@tiangong-lca/pcr 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @tiangong-lca/pcr
2
2
 
3
- Read TianGong LCA product category rules (PCRs), inspect methodology guidance,
4
- and validate foreground data drafts from a local content library. This package
3
+ Use TianGong LCA product category rules (PCRs) to guide LCA data creation,
4
+ optionally author TIDAS processes, and support Agent-led review of existing
5
+ TIDAS processes/models using a local content library. This package
5
6
  contains the CLI, schemas, bundled runtime dependencies, and a consumer Agent
6
7
  Skill. Install the content separately with
7
8
  [`@tiangong-lca/pcr-library`](https://www.npmjs.com/package/@tiangong-lca/pcr-library).
@@ -39,6 +40,10 @@ combined with `--library`.
39
40
  ./node_modules/.bin/tiangong-pcr coverage summary --classification cpc:3.0 --format json
40
41
  ./node_modules/.bin/tiangong-pcr resolve --classification cpc:3.0:01111 --format json
41
42
  ./node_modules/.bin/tiangong-pcr guidance --pcr <pcr-id> --format json
43
+ ./node_modules/.bin/tiangong-pcr guidance --pcr <pcr-id> --topic boundary --format json
44
+ ./node_modules/.bin/tiangong-pcr inspect --input process.json --related ./datasets --section exchanges --format json
45
+ ./node_modules/.bin/tiangong-pcr review prepare --pcr <pcr-id> --input process.json --related ./datasets --output review.json --format json
46
+ ./node_modules/.bin/tiangong-pcr review check --pcr <pcr-id> --input process.json --related ./datasets --report review.json --format json
42
47
  ./node_modules/.bin/tiangong-pcr --help
43
48
  ```
44
49
 
@@ -48,12 +53,26 @@ compatibility records. Guidance reports readiness and validation coverage:
48
53
  candidate content still requires review, and installing a package does not
49
54
  approve a methodology. The content package provides English documents only.
50
55
 
56
+ Selected guidance returns verified source hashes, applicability and JSON Pointers;
57
+ follow pagination or read complete values with `--pointer`. `inspect` reads native
58
+ TIDAS JSON and explicitly supplied local references without fetching URIs or
59
+ asserting schema validity. `calculate` performs normalization, conversion or
60
+ balance arithmetic with explicit quantities, bases and evidence; see its help.
61
+
62
+ The Agent fills the draft review after investigating the data. Findings distinguish
63
+ confirmed issues, suspected anomalies and evidence gaps, with input/PCR evidence
64
+ and unreviewed scope. `review check` validates the envelope and references;
65
+ `methodology_approval` remains false. A valid report does not certify its reasoning.
66
+ Use the separately provisioned TIDAS toolkit/SDK for format validation. A foreground
67
+ package is optional; legacy `validate-model` only checks qualifier text and
68
+ `validate-dataset` only checks collection protocol IDs.
69
+
51
70
  ## Fully offline installation
52
71
 
53
72
  On a connected machine, download both packages:
54
73
 
55
74
  ```sh
56
- npm pack @tiangong-lca/pcr@0.1.2
75
+ npm pack @tiangong-lca/pcr@0.2.0
57
76
  npm pack @tiangong-lca/pcr-library@0.1.2
58
77
  ```
59
78
 
@@ -61,7 +80,7 @@ Transfer the two tarballs and a suitable Node.js runtime to the offline machine.
61
80
  In the destination directory:
62
81
 
63
82
  ```sh
64
- npm install --offline --ignore-scripts --no-audit --no-fund ./tiangong-lca-pcr-0.1.2.tgz ./tiangong-lca-pcr-library-0.1.2.tgz
83
+ npm install --offline --ignore-scripts --no-audit --no-fund ./tiangong-lca-pcr-0.2.0.tgz ./tiangong-lca-pcr-library-0.1.2.tgz
65
84
  ./node_modules/.bin/tiangong-pcr library verify --format json
66
85
  ```
67
86
 
@@ -69,6 +88,8 @@ Runtime dependencies are bundled. No install scripts, runtime downloads, or
69
88
  network connection are required. Tool and content versions are independent;
70
89
  record both versions and the snapshot hash for reproducible work. Use
71
90
  `--library-sha256 sha256:<digest>` to require a specific trusted snapshot hash.
91
+ The CLI does not include an LLM. Fully offline semantic review requires an
92
+ offline-capable Agent/model supplied by the caller.
72
93
 
73
94
  ## Agent Skill
74
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiangong-lca/pcr",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Offline PCR consumer CLI and agent Skill",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "type": "git",
28
28
  "url": "git+https://github.com/tiangong-lca/pcr.git"
29
29
  },
30
- "gitHead": "02f5b58ca242035dcbce41845c4ea92dc8c63d25",
30
+ "gitHead": "f70d735639de9d21a24ff8959f0330880f969631",
31
31
  "publishConfig": {
32
32
  "access": "public",
33
33
  "registry": "https://registry.npmjs.org"
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tiangong-lca.org/schemas/pcr/v1/agent-review.schema.json",
4
+ "title": "PCR Agent review evidence envelope; never a methodology approval",
5
+ "type": "object",
6
+ "required": ["schema_version", "report_kind", "status", "pcr", "inputs", "scope", "coverage", "findings"],
7
+ "properties": {
8
+ "schema_version": { "const": 1 },
9
+ "report_kind": { "const": "tiangong-pcr-agent-review" },
10
+ "status": { "enum": ["draft", "reviewed"] },
11
+ "pcr": { "$ref": "#/$defs/pcrBinding" },
12
+ "inputs": { "type": "array", "minItems": 1, "items": {
13
+ "type": "object", "required": ["file", "sha256", "kind", "uuid", "version"],
14
+ "properties": {
15
+ "file": { "$ref": "#/$defs/text" }, "sha256": { "$ref": "#/$defs/hash" },
16
+ "kind": { "$ref": "#/$defs/text" }, "uuid": { "type": ["string", "null"] }, "version": { "type": ["string", "null"] }
17
+ }, "additionalProperties": false
18
+ } },
19
+ "scope": {
20
+ "type": "object", "required": ["description", "applicability", "limitations"],
21
+ "properties": {
22
+ "description": { "$ref": "#/$defs/text" }, "applicability": { "$ref": "#/$defs/text" },
23
+ "limitations": { "$ref": "#/$defs/texts" }
24
+ }, "additionalProperties": true
25
+ },
26
+ "coverage": { "type": "array", "minItems": 1, "items": {
27
+ "type": "object", "required": ["topic", "status", "rationale"],
28
+ "properties": {
29
+ "topic": { "$ref": "#/$defs/text" }, "status": { "enum": ["reviewed", "not_applicable", "not_reviewed"] },
30
+ "rationale": { "$ref": "#/$defs/text" }
31
+ }, "additionalProperties": true
32
+ } },
33
+ "findings": { "type": "array", "items": {
34
+ "type": "object", "required": ["id", "kind", "severity", "observation", "rationale", "input_refs", "pcr_refs", "suggested_action", "questions"],
35
+ "properties": {
36
+ "id": { "$ref": "#/$defs/text" },
37
+ "kind": { "enum": ["confirmed_issue", "suspected_anomaly", "evidence_gap"] },
38
+ "severity": { "enum": ["error", "warning", "info"] },
39
+ "observation": { "$ref": "#/$defs/text" }, "rationale": { "$ref": "#/$defs/text" },
40
+ "input_refs": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/inputRef" } },
41
+ "pcr_refs": { "type": "array", "items": { "allOf": [
42
+ { "$ref": "#/$defs/pcrBinding" },
43
+ { "type": "object", "required": ["pointer"], "properties": { "pointer": { "type": "string" } } }
44
+ ] } },
45
+ "suggested_action": { "$ref": "#/$defs/text" }, "questions": { "$ref": "#/$defs/texts" },
46
+ "alternative_explanations": { "$ref": "#/$defs/texts" }
47
+ }, "additionalProperties": true
48
+ } }
49
+ },
50
+ "additionalProperties": true,
51
+ "$defs": {
52
+ "text": { "type": "string", "minLength": 1, "pattern": "\\S" },
53
+ "texts": { "type": "array", "items": { "$ref": "#/$defs/text" } },
54
+ "hash": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
55
+ "inputRef": {
56
+ "type": "object", "required": ["file", "sha256", "pointer"],
57
+ "properties": { "file": { "$ref": "#/$defs/text" }, "sha256": { "$ref": "#/$defs/hash" }, "pointer": { "type": "string" } },
58
+ "additionalProperties": false
59
+ },
60
+ "pcrBinding": {
61
+ "type": "object", "required": ["pcr_id", "version", "projection_sha256", "markdown_sha256", "methodology_status"],
62
+ "properties": {
63
+ "pcr_id": { "$ref": "#/$defs/text" }, "version": { "type": ["string", "null"] },
64
+ "projection_sha256": { "$ref": "#/$defs/hash" }, "markdown_sha256": { "$ref": "#/$defs/hash" },
65
+ "methodology_status": { "$ref": "#/$defs/text" }
66
+ }, "additionalProperties": true
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,53 @@
1
+ import { ConsumptionError, object, strictNumber } from "./consumption-data.mjs";
2
+
3
+ function text(value, name) {
4
+ if (typeof value !== "string" || !value.trim()) throw new ConsumptionError("PCR_CALCULATION_INPUT", `${name} must state the unit, basis or evidence explicitly.`, { field: name });
5
+ return value;
6
+ }
7
+
8
+ // Arithmetic only: the Agent owns physical equivalence, applicability and evidence.
9
+ export function calculate(request) {
10
+ if (!object(request)) throw new ConsumptionError("PCR_CALCULATION_INPUT", "Use a JSON object with operation normalize, convert or balance. See calculate --help.");
11
+ const evidence = text(request.evidence, "evidence");
12
+ const basis = text(request.basis, "basis");
13
+ let result;
14
+ let formula;
15
+ if (request.operation === "normalize") {
16
+ const amount = strictNumber(request.amount, "amount");
17
+ const sourceReference = strictNumber(request.source_reference, "source_reference", { positive: true });
18
+ const targetReference = strictNumber(request.target_reference, "target_reference", { positive: true });
19
+ const unit = text(request.unit, "unit");
20
+ text(request.reference_unit, "reference_unit");
21
+ result = { amount: amount * targetReference / sourceReference, unit, factor: targetReference / sourceReference };
22
+ formula = "amount * target_reference / source_reference; both reference quantities share reference_unit and the stated physical basis";
23
+ } else if (request.operation === "convert") {
24
+ const amount = strictNumber(request.amount, "amount");
25
+ const factor = strictNumber(request.factor, "factor", { positive: true });
26
+ text(request.from_unit, "from_unit");
27
+ const unit = text(request.to_unit, "to_unit");
28
+ result = { amount: amount * factor, unit, factor };
29
+ formula = "amount * factor; factor is explicitly supplied in to_unit / from_unit";
30
+ } else if (request.operation === "balance") {
31
+ const unit = text(request.unit, "unit");
32
+ const sums = {};
33
+ for (const name of ["inputs", "outputs"]) {
34
+ if (!Array.isArray(request[name]) || !request[name].length) throw new ConsumptionError("PCR_CALCULATION_INPUT", `${name} must be a non-empty array of {amount, label} in the declared common unit and basis.`);
35
+ sums[name] = request[name].reduce((sum, item, index) => {
36
+ if (!object(item)) throw new ConsumptionError("PCR_CALCULATION_INPUT", `${name}[${index}] must be an amount/label object.`);
37
+ text(item.label, `${name}[${index}].label`);
38
+ return sum + strictNumber(item.amount, `${name}[${index}].amount`);
39
+ }, 0);
40
+ }
41
+ const accumulation = strictNumber(request.accumulation, "accumulation");
42
+ result = { input_total: sums.inputs, output_total: sums.outputs, accumulation, residual: sums.inputs - sums.outputs - accumulation, unit };
43
+ formula = "sum(inputs) - sum(outputs) - accumulation; positive residual is unaccounted net input";
44
+ } else {
45
+ throw new ConsumptionError("PCR_CALCULATION_OPERATION", "Use operation normalize, convert or balance. No formulas, factors or units are inferred.");
46
+ }
47
+ if (Object.values(result).some((value) => typeof value === "number" && !Number.isFinite(value))) throw new ConsumptionError("PCR_CALCULATION_OVERFLOW", "Calculation produced a non-finite number; rescale or correct the input quantities.");
48
+ return {
49
+ schema_version: 1, calculation_kind: "tiangong-pcr-arithmetic", operation: request.operation,
50
+ request, formula, result, basis, evidence,
51
+ interpretation: "Arithmetic only, using supplied values and JavaScript floating-point precision. Physical applicability, evidence quality, tolerance and anomaly judgment are not evaluated.",
52
+ };
53
+ }
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export class ConsumptionError extends Error {
6
+ constructor(code, message, details = {}) {
7
+ super(message);
8
+ this.name = "ConsumptionError";
9
+ this.code = code;
10
+ this.details = details;
11
+ }
12
+ }
13
+
14
+ export const sha256 = (bytes) => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
15
+ export const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ export const pointerToken = (value) => String(value).replaceAll("~", "~0").replaceAll("/", "~1");
17
+
18
+ export function atPointer(value, pointer) {
19
+ if (typeof pointer !== "string" || (pointer !== "" && !pointer.startsWith("/")) || /~(?:[^01]|$)/u.test(pointer)) {
20
+ throw new ConsumptionError("PCR_POINTER_INVALID", "Use an RFC 6901 JSON Pointer, such as /processDataSet/exchanges/exchange/0.");
21
+ }
22
+ if (pointer === "") return value;
23
+ let current = value;
24
+ for (const encoded of pointer.slice(1).split("/")) {
25
+ const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
26
+ if (current === null || typeof current !== "object" || !Object.hasOwn(current, key)
27
+ || (Array.isArray(current) && !/^(0|[1-9]\d*)$/u.test(key))) {
28
+ throw new ConsumptionError("PCR_POINTER_NOT_FOUND", `No value exists at ${pointer}. Inspect the summary or a paged section first.`, { pointer });
29
+ }
30
+ current = current[key];
31
+ }
32
+ return current;
33
+ }
34
+
35
+ export function readJsonDocument(filename, maxBytes = 16 * 1024 * 1024) {
36
+ const file = path.resolve(filename);
37
+ let bytes;
38
+ try {
39
+ const stat = statSync(file);
40
+ if (!stat.isFile() || stat.size > maxBytes) {
41
+ throw new ConsumptionError("PCR_INPUT_SIZE", `Expected a JSON file no larger than ${maxBytes} bytes: ${file}`);
42
+ }
43
+ bytes = readFileSync(file);
44
+ if (bytes.length > maxBytes) throw new ConsumptionError("PCR_INPUT_SIZE", `Input grew beyond ${maxBytes} bytes: ${file}`);
45
+ } catch (error) {
46
+ if (error instanceof ConsumptionError) throw error;
47
+ throw new ConsumptionError("PCR_INPUT_READ", `Cannot read local JSON file ${file}: ${error.message}`, { file });
48
+ }
49
+ try {
50
+ return { file, sha256: sha256(bytes), bytes: bytes.length, value: JSON.parse(bytes.toString("utf8").replace(/^\uFEFF/u, "")) };
51
+ } catch (error) {
52
+ throw new ConsumptionError("PCR_INPUT_JSON", `Malformed JSON in ${file}: ${error.message}`, { file });
53
+ }
54
+ }
55
+
56
+ export function entriesAt(value, pointer) {
57
+ if (value === undefined || value === null) return [];
58
+ return Array.isArray(value)
59
+ ? value.map((item, index) => ({ pointer: `${pointer}/${index}`, value: item }))
60
+ : [{ pointer, value }];
61
+ }
62
+
63
+ export function paginate(items, page = 1, pageSize = 10) {
64
+ if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
65
+ throw new ConsumptionError("PCR_PAGE_INVALID", "Use --page >= 1 and --page-size from 1 to 100.");
66
+ }
67
+ const pages = Math.max(1, Math.ceil(items.length / pageSize));
68
+ if (page > pages) throw new ConsumptionError("PCR_PAGE_RANGE", `Page ${page} exceeds ${pages}; use --page ${pages}.`);
69
+ return {
70
+ items: items.slice((page - 1) * pageSize, page * pageSize),
71
+ pagination: { page, page_size: pageSize, total: items.length, total_pages: pages, has_more: page < pages },
72
+ };
73
+ }
74
+
75
+ export function preview(value, maxChars = 2400) {
76
+ const json = JSON.stringify(value);
77
+ return json.length <= maxChars ? { value, truncated: false }
78
+ : { excerpt: json.slice(0, maxChars), truncated: true, total_characters: json.length };
79
+ }
80
+
81
+ export function strictNumber(value, name, { positive = false } = {}) {
82
+ if ((typeof value !== "number" && typeof value !== "string")
83
+ || (typeof value === "string" && !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u.test(value))
84
+ || !Number.isFinite(Number(value)) || (positive && Number(value) <= 0)) {
85
+ throw new ConsumptionError("PCR_NUMBER_INVALID", `${name} must be a finite${positive ? " positive" : ""} decimal number. Missing values are not zero.`, { field: name });
86
+ }
87
+ return Number(value);
88
+ }
@@ -0,0 +1,62 @@
1
+ import { getVerifiedPcrProjection } from "./index.mjs";
2
+ import { ConsumptionError, atPointer, entriesAt, paginate, preview } from "./consumption-data.mjs";
3
+
4
+ const TOPICS = {
5
+ overview: ["product_category_identity", "functional_unit", "boundary_abstraction"],
6
+ "reference-flow": ["functional_unit", "reference_flow_definition"],
7
+ boundary: ["system_boundary/rules", "boundary_abstraction"],
8
+ measurement: ["measurement_rules"],
9
+ inventory: ["process_map", "process_inventory"],
10
+ collection: ["dataset_production/collection_protocols"],
11
+ calculation: ["dataset_production/calculation_rules"],
12
+ allocation: ["allocation_rules"],
13
+ quality: ["data_quality_rules", "dataset_production/data_quality_requirements", "published_dataset_profile"],
14
+ validation: ["validation_rules"],
15
+ sources: ["data_sources"],
16
+ };
17
+ export const GUIDANCE_TOPICS = Object.keys(TOPICS);
18
+
19
+ export function projectionBinding(snapshot) {
20
+ return {
21
+ pcr_id: snapshot.pcr.id,
22
+ version: snapshot.pcr.version,
23
+ projection_sha256: snapshot.structured.projection_metadata.generated_content_sha256,
24
+ markdown_sha256: snapshot.structured.projection_metadata.canonical_markdown.sha256,
25
+ methodology_status: snapshot.readiness.methodology_status,
26
+ };
27
+ }
28
+
29
+ export function pcrEvidence(snapshot, pointer) {
30
+ return { ...projectionBinding(snapshot), pointer };
31
+ }
32
+
33
+ export function selectGuidance({ root, pcrId, topic = "overview", pointer, page = 1, pageSize = 10 }) {
34
+ const snapshot = getVerifiedPcrProjection({ root, pcrId });
35
+ const base = {
36
+ schema_version: 1, guidance_kind: "tiangong-pcr-guidance-selection",
37
+ pcr: projectionBinding(snapshot), readiness: snapshot.readiness,
38
+ source_structured: snapshot.source_structured,
39
+ source_markdown: snapshot.source_structured.replace(/structured\.yaml$/u, "pcr.en-US.md"),
40
+ topics: GUIDANCE_TOPICS,
41
+ citation_note: "Pointers address this verified structured projection. Preserve its hashes; applicability remains an Agent judgment. Explicit rule IDs may persist across revisions; array pointers are snapshot-bound.",
42
+ };
43
+ if (pointer !== undefined) {
44
+ return { ...base, source: pcrEvidence(snapshot, pointer), value: atPointer(snapshot.structured, pointer) };
45
+ }
46
+ if (!Object.hasOwn(TOPICS, topic)) throw new ConsumptionError("PCR_GUIDANCE_TOPIC", `Use --topic ${GUIDANCE_TOPICS.join("|")}.`);
47
+ const entries = [];
48
+ for (const name of TOPICS[topic]) {
49
+ const pointer = `/${name}`;
50
+ let value;
51
+ try { value = atPointer(snapshot.structured, pointer); }
52
+ catch (error) { if (error.code !== "PCR_POINTER_NOT_FOUND") throw error; }
53
+ for (const entry of entriesAt(value, pointer)) {
54
+ entries.push({
55
+ source: pcrEvidence(snapshot, entry.pointer),
56
+ rule_id: entry.value?.rule_id ?? entry.value?.protocol_id ?? entry.value?.row_id ?? entry.value?.id ?? null,
57
+ ...preview(entry.value),
58
+ });
59
+ }
60
+ }
61
+ return { ...base, topic, ...paginate(entries, page, pageSize) };
62
+ }
@@ -0,0 +1,83 @@
1
+ import { atPointer } from "./consumption-data.mjs";
2
+ import { getVerifiedPcrProjection } from "./index.mjs";
3
+ import { projectionBinding } from "./consumption-guidance.mjs";
4
+ import { documentIdentity, extractReferences, loadTidasContext } from "./tidas-inspection.mjs";
5
+ import { validateCoreContract } from "./contracts.mjs";
6
+
7
+ const TOPICS = ["scope_and_applicability", "reference_basis", "inventory_and_boundary", "allocation", "data_quality"];
8
+
9
+ export function prepareReview({ root, pcrId, input, related }) {
10
+ const snapshot = getVerifiedPcrProjection({ root, pcrId });
11
+ const context = loadTidasContext({ input, related });
12
+ const unresolved = extractReferences(context).filter((reference) => reference.status !== "resolved");
13
+ return {
14
+ schema_version: 1, report_kind: "tiangong-pcr-agent-review", status: "draft",
15
+ pcr: projectionBinding(snapshot), inputs: context.documents.map(documentIdentity),
16
+ scope: {
17
+ description: `Review of supplied ${context.primary.kind}; the Agent must establish its declared scope and reference basis.`,
18
+ applicability: "Not assessed. Explain product, technology, gate and PCR applicability before drawing conclusions.",
19
+ limitations: [
20
+ "No semantic review or TIDAS schema validation has been performed by this command.",
21
+ `${unresolved.length} reference(s) could not be uniquely resolved in the supplied local inputs; inspect references before claiming omissions.`,
22
+ `${context.ignoredFiles.length} JSON file(s) with no native TIDAS dataset root were ignored by input inspection.`,
23
+ `PCR methodology status: ${snapshot.readiness.methodology_status}; library availability does not promote candidate methodology.`,
24
+ ],
25
+ },
26
+ coverage: TOPICS.map((topic) => ({ topic, status: "not_reviewed", rationale: "Awaiting Agent investigation of applicability and evidence." })),
27
+ findings: [],
28
+ };
29
+ }
30
+
31
+ export function checkReview({ root, pcrId, input, related, report }) {
32
+ const shape = validateCoreContract("agent-review.schema.json", report);
33
+ const issues = [];
34
+ const add = (code, location, message) => issues.push({ code, location, message });
35
+ if (!shape.valid) {
36
+ for (const error of shape.issues ?? shape.errors) add("report_shape", error.instance_path ?? error.instancePath ?? "", error.message);
37
+ } else {
38
+ const snapshot = getVerifiedPcrProjection({ root, pcrId });
39
+ const binding = projectionBinding(snapshot);
40
+ const context = loadTidasContext({ input, related });
41
+ const actual = context.documents.map(documentIdentity);
42
+ const files = new Map(context.documents.map((doc) => [doc.file, doc]));
43
+ const sameBinding = (candidate) => Object.keys(binding).every((key) => candidate[key] === binding[key]);
44
+ if (!sameBinding(report.pcr)) add("pcr_changed", "/pcr", "PCR identity, content hashes or methodology status differ from the selected verified projection.");
45
+ if (report.inputs.length !== actual.length || report.inputs.some((item, index) => Object.keys(actual[index] ?? {}).some((key) => item[key] !== actual[index][key]))) {
46
+ add("inputs_changed", "/inputs", "Input files, identities, order or exact-byte hashes differ. Regenerate the draft bindings and review the changed evidence.");
47
+ }
48
+ if (report.status === "reviewed" && !report.coverage.some((item) => item.status === "reviewed")) add("review_empty", "/coverage", "A reviewed report must name at least one actually reviewed topic; otherwise retain draft status.");
49
+ const ids = new Set();
50
+ for (const [index, finding] of report.findings.entries()) {
51
+ const prefix = `/findings/${index}`;
52
+ if (ids.has(finding.id)) add("duplicate_finding", `${prefix}/id`, "Finding IDs must be unique within this report.");
53
+ ids.add(finding.id);
54
+ for (const [refIndex, reference] of finding.input_refs.entries()) {
55
+ const location = `${prefix}/input_refs/${refIndex}`;
56
+ const doc = files.get(reference.file);
57
+ if (!doc || reference.sha256 !== doc.sha256) add("input_reference_changed", location, "Reference must identify an explicitly supplied input and its current exact-byte hash.");
58
+ else {
59
+ try { atPointer(doc.value, reference.pointer); }
60
+ catch (error) { add("input_pointer", location, error.message); }
61
+ }
62
+ }
63
+ for (const [refIndex, reference] of finding.pcr_refs.entries()) {
64
+ const location = `${prefix}/pcr_refs/${refIndex}`;
65
+ if (!sameBinding(reference)) add("pcr_reference_changed", location, "PCR reference does not match the selected verified projection.");
66
+ else {
67
+ try { atPointer(snapshot.structured, reference.pointer); }
68
+ catch (error) { add("pcr_pointer", location, error.message); }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ return {
74
+ schema_version: 1, check_kind: "tiangong-pcr-review-envelope-check",
75
+ envelope_valid: issues.length === 0, methodology_approval: false,
76
+ report_status: shape.valid ? report.status : null,
77
+ checked: ["report_shape", ...(shape.valid ? ["input_and_pcr_bindings", "finding_reference_locations", "review_status_consistency"] : [])],
78
+ not_checked: ["PCR applicability", "truth of observations or conclusions", "sufficiency of evidence", "coverage completeness", "TIDAS schema validity"],
79
+ issues,
80
+ next_step: issues.length ? "Correct the report or re-review changed evidence, then run review check again."
81
+ : "Have the Agent/user assess the reasoning and remaining scope. Envelope validity does not approve the dataset.",
82
+ };
83
+ }
@@ -8,6 +8,7 @@ import {
8
8
 
9
9
  const CORE_SCHEMA_DEPENDENCY_FILES = ["controlled-vocabulary.schema.json"];
10
10
  const CORE_SCHEMA_FILES = [
11
+ "agent-review.schema.json",
11
12
  "classification-coverage.schema.json",
12
13
  "dataset-validation-input.schema.json",
13
14
  "feedback-draft-output.schema.json",
@@ -29,6 +30,7 @@ const schemaDependencies = CORE_SCHEMA_DEPENDENCY_FILES.map((fileName) =>
29
30
  );
30
31
  const contractIds = new Map(schemaEntries.map(({ fileName, schema }) => [fileName, schema.$id]));
31
32
  const entityKindsByFile = {
33
+ "agent-review.schema.json": "agent_review",
32
34
  "classification-coverage.schema.json": "classification_coverage",
33
35
  "dataset-validation-input.schema.json": "dataset_validation_input",
34
36
  "feedback-draft-output.schema.json": "feedback_draft_output",
@@ -738,9 +738,9 @@ function buildGuidanceForOperation({ root, pcrId, operation, context = null }) {
738
738
  validation_rules: structured.validation_rules ?? [],
739
739
  data_sources: structured.data_sources ?? [],
740
740
  validation_notes: [
741
- "Use this guidance as the source of Tiangong foreground data collection package requirements.",
742
- "Preserve Tiangong UUIDs exactly and keep PCR-derived UUID references version-free.",
743
- "Run tiangong-pcr validate-dataset after constructing a foreground data package and draft feedback if PCR guidance is missing or ambiguous.",
741
+ "Use this guidance for LCA data authoring, optional TIDAS process authoring, or Agent-led review of existing process/model data. Select requirements for the declared scope.",
742
+ "Preserve PCR-derived UUID identities exactly. They are version-free suggestions; preserve and verify explicit versions on supplied TIDAS dataset references.",
743
+ "validate-model checks qualifier text presence; validate-dataset checks collection protocol ID presence. Neither performs TIDAS schema validation or semantic review. Use inspect and review prepare/check to support Agent-led review; a foreground package is optional.",
744
744
  ],
745
745
  };
746
746
  assertCoreContract("guidance-output.schema.json", guidance, {