@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 +25 -4
- package/package.json +2 -2
- package/packages/pcr-core/schemas/agent-review.schema.json +69 -0
- package/packages/pcr-core/src/consumption-calculation.mjs +53 -0
- package/packages/pcr-core/src/consumption-data.mjs +88 -0
- package/packages/pcr-core/src/consumption-guidance.mjs +62 -0
- package/packages/pcr-core/src/consumption-review.mjs +83 -0
- package/packages/pcr-core/src/contracts.mjs +2 -0
- package/packages/pcr-core/src/index.mjs +3 -3
- package/packages/pcr-core/src/tidas-inspection.mjs +177 -0
- package/packages/tiangong-pcr-cli/src/commands.mjs +215 -10
- package/skills/tiangong-pcr/SKILL.md +84 -111
- package/skills/tiangong-pcr/references/cli-usage.md +60 -13
- package/skills/tiangong-pcr/references/lca-authoring.md +30 -0
- package/skills/tiangong-pcr/references/model-authoring-loop.md +5 -12
- package/skills/tiangong-pcr/references/reviewing-tidas.md +71 -0
- package/skills/tiangong-pcr/references/tidas-authoring.md +45 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ConsumptionError, atPointer, entriesAt, object, paginate, pointerToken, preview, readJsonDocument } from "./consumption-data.mjs";
|
|
4
|
+
|
|
5
|
+
// Field paths, not a second TIDAS schema or consumer validation policy.
|
|
6
|
+
// Basis: public-spec candidate 0.2.3, revision f118660dbcbfbf736be74837cce0bf26cd177245.
|
|
7
|
+
const DATASETS = [
|
|
8
|
+
["process", "processDataSet", "processInformation", "process data set"],
|
|
9
|
+
["lifecyclemodel", "lifeCycleModelDataSet", "lifeCycleModelInformation", "life cycle model data set"],
|
|
10
|
+
["flow", "flowDataSet", "flowInformation", "flow data set"],
|
|
11
|
+
["flowproperty", "flowPropertyDataSet", "flowPropertiesInformation", "flow property data set"],
|
|
12
|
+
["unitgroup", "unitGroupDataSet", "unitGroupInformation", "unit group data set"],
|
|
13
|
+
["source", "sourceDataSet", "sourceInformation", "source data set"],
|
|
14
|
+
["contact", "contactDataSet", "contactInformation", "contact data set"],
|
|
15
|
+
["lciamethod", "LCIAMethodDataSet", "LCIAMethodInformation", "LCIA method data set"],
|
|
16
|
+
];
|
|
17
|
+
export const INSPECTION_SECTIONS = ["summary", "exchanges", "instances", "references", "documents"];
|
|
18
|
+
const MAX_DOCUMENTS = 200;
|
|
19
|
+
const MAX_BYTES = 64 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
function identify(document, required = true) {
|
|
22
|
+
const matches = DATASETS.filter(([, root]) => object(document.value) && Object.hasOwn(document.value, root));
|
|
23
|
+
if (matches.length !== 1 || !object(document.value[matches[0]?.[1]])) {
|
|
24
|
+
if (!required && matches.length === 0) return null;
|
|
25
|
+
throw new ConsumptionError("PCR_TIDAS_SHAPE", `Expected one native TIDAS dataset root in ${document.file}. Wrapped database rows must be exported as native dataset JSON first.`, { roots: DATASETS.map(([, root]) => root) });
|
|
26
|
+
}
|
|
27
|
+
const [kind, root, informationKey, referenceType] = matches[0];
|
|
28
|
+
const body = document.value[root];
|
|
29
|
+
const info = body[informationKey];
|
|
30
|
+
const uuid = info?.dataSetInformation?.["common:UUID"] ?? null;
|
|
31
|
+
const version = body.administrativeInformation?.publicationAndOwnership?.["common:dataSetVersion"] ?? null;
|
|
32
|
+
if ((uuid !== null && typeof uuid !== "string") || (version !== null && typeof version !== "string")) {
|
|
33
|
+
throw new ConsumptionError("PCR_TIDAS_IDENTITY", `Dataset UUID/version must be strings when supplied in ${document.file}. Inspect the native identity fields or run the TIDAS format validator.`, {
|
|
34
|
+
uuid_pointer: `/${root}/${informationKey}/dataSetInformation/common:UUID`,
|
|
35
|
+
version_pointer: `/${root}/administrativeInformation/publicationAndOwnership/common:dataSetVersion`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...document, kind, root, informationKey, referenceType,
|
|
40
|
+
uuid, version,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function documentIdentity(doc) {
|
|
45
|
+
return { file: doc.file, sha256: doc.sha256, kind: doc.kind, uuid: doc.uuid, version: doc.version };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function relatedFiles(directory) {
|
|
49
|
+
const base = path.resolve(directory);
|
|
50
|
+
const files = [];
|
|
51
|
+
try {
|
|
52
|
+
if (!statSync(base).isDirectory()) throw new Error("not a directory");
|
|
53
|
+
let visited = 0;
|
|
54
|
+
function visit(current, depth) {
|
|
55
|
+
if (depth > 12) throw new ConsumptionError("PCR_RELATED_LIMIT", "Related directory nesting exceeds 12 levels. Select a smaller local package.");
|
|
56
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
57
|
+
if (++visited > 5000) throw new ConsumptionError("PCR_RELATED_LIMIT", "Related directory exceeds 5000 entries. Select a smaller local package.");
|
|
58
|
+
const file = path.join(current, entry.name);
|
|
59
|
+
if (entry.isSymbolicLink()) throw new ConsumptionError("PCR_RELATED_SYMLINK", `Related packages must contain ordinary files/directories, not symlinks: ${file}`);
|
|
60
|
+
if (entry.isDirectory()) visit(file, depth + 1);
|
|
61
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
62
|
+
files.push(file);
|
|
63
|
+
if (files.length > MAX_DOCUMENTS) throw new ConsumptionError("PCR_RELATED_LIMIT", `More than ${MAX_DOCUMENTS} JSON files. Select a smaller related package.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
visit(base, 0);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof ConsumptionError) throw error;
|
|
70
|
+
throw new ConsumptionError("PCR_RELATED_READ", `Cannot inspect --related directory ${base}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadTidasContext({ input, related = null }) {
|
|
76
|
+
const primary = identify(readJsonDocument(input));
|
|
77
|
+
const documents = [primary];
|
|
78
|
+
const ignoredFiles = [];
|
|
79
|
+
let totalBytes = primary.bytes;
|
|
80
|
+
const primaryRealPath = realpathSync(primary.file);
|
|
81
|
+
for (const file of related ? relatedFiles(related) : []) {
|
|
82
|
+
if (realpathSync(file) === primaryRealPath) continue;
|
|
83
|
+
const raw = readJsonDocument(file);
|
|
84
|
+
totalBytes += raw.bytes;
|
|
85
|
+
if (totalBytes > MAX_BYTES) throw new ConsumptionError("PCR_RELATED_LIMIT", `Related input exceeds ${MAX_BYTES} bytes. Select a smaller local package.`);
|
|
86
|
+
const doc = identify(raw, false);
|
|
87
|
+
if (doc) documents.push(doc);
|
|
88
|
+
else ignoredFiles.push({ file: raw.file, sha256: raw.sha256, reason: "No recognized native TIDAS dataset root." });
|
|
89
|
+
}
|
|
90
|
+
return { primary, documents, ignoredFiles, related: related ? path.resolve(related) : null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function inputEvidence(doc, pointer) {
|
|
94
|
+
return { file: doc.file, sha256: doc.sha256, pointer };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function extractReferences(context) {
|
|
98
|
+
const references = [];
|
|
99
|
+
for (const doc of context.documents) {
|
|
100
|
+
const stack = [{ value: doc.value, pointer: "" }];
|
|
101
|
+
while (stack.length) {
|
|
102
|
+
const { value, pointer } = stack.pop();
|
|
103
|
+
if (value === null || typeof value !== "object") continue;
|
|
104
|
+
if (object(value) && (Object.hasOwn(value, "@refObjectId") || (Object.hasOwn(value, "@type") && Object.hasOwn(value, "@uri")))) {
|
|
105
|
+
const uuid = value["@refObjectId"] ?? null;
|
|
106
|
+
const version = value["@version"] ?? null;
|
|
107
|
+
const type = value["@type"] ?? null;
|
|
108
|
+
const kind = DATASETS.find((entry) => entry[3] === type)?.[0];
|
|
109
|
+
const usableIdentity = typeof uuid === "string" && uuid.length > 0 && kind !== undefined
|
|
110
|
+
&& (!Object.hasOwn(value, "@version") || (typeof version === "string" && version.length > 0));
|
|
111
|
+
const sameIdentity = usableIdentity ? context.documents.filter((candidate) => candidate.kind === kind && candidate.uuid === uuid) : [];
|
|
112
|
+
const candidates = sameIdentity.filter((candidate) => version === null || candidate.version === version);
|
|
113
|
+
const status = !usableIdentity ? "unresolved_identity" : candidates.length > 1 ? "ambiguous"
|
|
114
|
+
: candidates.length === 1 ? "resolved" : sameIdentity.length ? "version_mismatch" : "missing";
|
|
115
|
+
references.push({
|
|
116
|
+
source: inputEvidence(doc, pointer), uuid, version, type, uri: value["@uri"] ?? null,
|
|
117
|
+
status, version_selection: version === null ? "unspecified" : "exact",
|
|
118
|
+
candidates: (candidates.length ? candidates : sameIdentity).map(documentIdentity),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const entries = Object.entries(value);
|
|
122
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
123
|
+
const [key, child] = entries[index];
|
|
124
|
+
if (child !== null && typeof child === "object") stack.push({ value: child, pointer: `${pointer}/${pointerToken(key)}` });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return references;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function inspectTidas({ input, related, section = "summary", pointer, page = 1, pageSize = 10 }) {
|
|
132
|
+
const context = loadTidasContext({ input, related });
|
|
133
|
+
const { primary } = context;
|
|
134
|
+
const references = extractReferences(context);
|
|
135
|
+
const report = {
|
|
136
|
+
schema_version: 1, inspection_kind: "tiangong-pcr-tidas-inspection",
|
|
137
|
+
primary: documentIdentity(primary), related_directory: context.related,
|
|
138
|
+
schema_validation: "not_performed",
|
|
139
|
+
scope: "Only explicitly supplied local JSON datasets; no URI is fetched or followed.",
|
|
140
|
+
counts: {
|
|
141
|
+
documents: context.documents.length, ignored_json_files: context.ignoredFiles.length,
|
|
142
|
+
references: references.length, resolved: references.filter((ref) => ref.status === "resolved").length,
|
|
143
|
+
unresolved: references.filter((ref) => ref.status !== "resolved").length,
|
|
144
|
+
},
|
|
145
|
+
limitations: [
|
|
146
|
+
"Inspection exposes fields and local references; it does not establish TIDAS validity or PCR compliance.",
|
|
147
|
+
"Missing local evidence is not proof that a lifecycle operation or upstream dataset does not exist.",
|
|
148
|
+
"Exchange units require flow, flow-property and unit-group evidence; names or amounts alone do not establish units.",
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
if (pointer !== undefined) {
|
|
152
|
+
return { ...report, section: "pointer", source: inputEvidence(primary, pointer), value: atPointer(primary.value, pointer) };
|
|
153
|
+
}
|
|
154
|
+
if (!INSPECTION_SECTIONS.includes(section)) throw new ConsumptionError("PCR_INSPECT_SECTION", `Use --section ${INSPECTION_SECTIONS.join("|")}.`);
|
|
155
|
+
if (section === "summary") {
|
|
156
|
+
const infoPath = `/${primary.root}/${primary.informationKey}`;
|
|
157
|
+
const info = primary.value[primary.root][primary.informationKey] ?? {};
|
|
158
|
+
const fields = ["dataSetInformation", "quantitativeReference", "time", "geography", "technology"]
|
|
159
|
+
.filter((key) => Object.hasOwn(info, key))
|
|
160
|
+
.map((key) => ({ name: key, source: inputEvidence(primary, `${infoPath}/${key}`), ...preview(info[key]) }));
|
|
161
|
+
return { ...report, section, fields, ignored_files: context.ignoredFiles.slice(0, 10), ignored_files_truncated: context.ignoredFiles.length > 10 };
|
|
162
|
+
}
|
|
163
|
+
let items;
|
|
164
|
+
if (section === "documents") items = context.documents.map(documentIdentity);
|
|
165
|
+
else if (section === "references") items = references;
|
|
166
|
+
else {
|
|
167
|
+
const expectedKind = section === "exchanges" ? "process" : "lifecyclemodel";
|
|
168
|
+
if (primary.kind !== expectedKind) throw new ConsumptionError("PCR_INSPECT_SECTION", `${section} requires a ${expectedKind} input. Use --section summary or --pointer for this ${primary.kind}.`);
|
|
169
|
+
const pointer = section === "exchanges" ? "/processDataSet/exchanges/exchange"
|
|
170
|
+
: "/lifeCycleModelDataSet/lifeCycleModelInformation/technology/processes/processInstance";
|
|
171
|
+
let value;
|
|
172
|
+
try { value = atPointer(primary.value, pointer); }
|
|
173
|
+
catch (error) { if (error.code !== "PCR_POINTER_NOT_FOUND") throw error; }
|
|
174
|
+
items = entriesAt(value, pointer).map((entry) => ({ source: inputEvidence(primary, entry.pointer), ...preview(entry.value) }));
|
|
175
|
+
}
|
|
176
|
+
return { ...report, section, ...paginate(items, page, pageSize) };
|
|
177
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { OfflineLibrary } from "../../pcr-core/src/offline-library.mjs";
|
|
3
3
|
import { withPcrSource } from "../../pcr-core/src/source-context.mjs";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { GUIDANCE_TOPICS, selectGuidance } from "../../pcr-core/src/consumption-guidance.mjs";
|
|
8
|
+
import { inspectTidas, INSPECTION_SECTIONS } from "../../pcr-core/src/tidas-inspection.mjs";
|
|
9
|
+
import { prepareReview, checkReview } from "../../pcr-core/src/consumption-review.mjs";
|
|
10
|
+
import { calculate } from "../../pcr-core/src/consumption-calculation.mjs";
|
|
11
|
+
import { readJsonDocument, sha256 } from "../../pcr-core/src/consumption-data.mjs";
|
|
7
12
|
|
|
8
13
|
import {
|
|
9
14
|
CLASSIFICATION_COVERAGE_STATUSES,
|
|
@@ -46,7 +51,12 @@ const COMMAND_OPTIONS = {
|
|
|
46
51
|
"coverage:list": new Set(["classification", "status", "page", "page-size"]),
|
|
47
52
|
resolve: new Set(["classification", "pcr"]),
|
|
48
53
|
show: new Set(["pcr", "lang"]),
|
|
49
|
-
guidance: new Set(["pcr"]),
|
|
54
|
+
guidance: new Set(["pcr", "topic", "pointer", "page", "page-size", "output"]),
|
|
55
|
+
inspect: new Set(["input", "related", "section", "pointer", "page", "page-size", "output"]),
|
|
56
|
+
calculate: new Set(["input", "output"]),
|
|
57
|
+
review: new Set(),
|
|
58
|
+
"review:prepare": new Set(["pcr", "input", "related", "output"]),
|
|
59
|
+
"review:check": new Set(["pcr", "input", "related", "report", "output"]),
|
|
50
60
|
"validate-model": new Set(["pcr", "input", "fail-on"]),
|
|
51
61
|
"validate-dataset": new Set(["pcr", "input", "fail-on"]),
|
|
52
62
|
"feedback:draft": new Set([
|
|
@@ -84,6 +94,11 @@ const COMMAND_DEFINITIONS = [
|
|
|
84
94
|
{ key: "resolve", command: "resolve", formats: ["json"], defaultFormat: "json" },
|
|
85
95
|
{ key: "show", command: "show", formats: ["markdown"], defaultFormat: "markdown" },
|
|
86
96
|
{ key: "guidance", command: "guidance", formats: ["json"], defaultFormat: "json" },
|
|
97
|
+
{ key: "inspect", command: "inspect", formats: ["json"], defaultFormat: "json" },
|
|
98
|
+
{ key: "calculate", command: "calculate", formats: ["json"], defaultFormat: "json" },
|
|
99
|
+
{ key: "review", command: "review", formats: ["json"], defaultFormat: "json" },
|
|
100
|
+
{ key: "review:prepare", command: "review", positional: ["prepare"], formats: ["json"], defaultFormat: "json" },
|
|
101
|
+
{ key: "review:check", command: "review", positional: ["check"], formats: ["json"], defaultFormat: "json" },
|
|
87
102
|
{ key: "validate-model", command: "validate-model", formats: ["json"], defaultFormat: "json" },
|
|
88
103
|
{ key: "validate-dataset", command: "validate-dataset", formats: ["json"], defaultFormat: "json" },
|
|
89
104
|
{
|
|
@@ -125,6 +140,10 @@ export function runTiangongPcr(argv) {
|
|
|
125
140
|
if (!command || command === "help" || options.help) return runCommand(argv);
|
|
126
141
|
if (command === "library" && positional.length === 0) throw new CliError("PCR_CLI_MISSING_SUBCOMMAND", "Use library info or library verify with --library <file>.");
|
|
127
142
|
if (options.root && options.library) throw new CliError("PCR_CLI_SOURCE_CONFLICT", "Choose either --root or --library.");
|
|
143
|
+
if (["inspect", "calculate"].includes(command)) {
|
|
144
|
+
if (options.library || options["library-sha256"]) throw new CliError("PCR_CLI_SOURCE_UNUSED", `${command} uses local input only; omit --library and --library-sha256.`);
|
|
145
|
+
return runCommand(argv);
|
|
146
|
+
}
|
|
128
147
|
const filename = options.library ?? (options.root ? null : process.env.PCR_LIBRARY ?? (existsSync(path.join(defaultRoot, "library/catalog.yaml")) ? null : installedLibrary()));
|
|
129
148
|
if (!filename) {
|
|
130
149
|
if (options["library-sha256"] || command === "library") throw new CliError("PCR_LIBRARY_REQUIRED", "Select a local snapshot with --library <library.sqlite>.");
|
|
@@ -234,8 +253,39 @@ function runCommand(argv, selected = null) {
|
|
|
234
253
|
}
|
|
235
254
|
if (command === "guidance") {
|
|
236
255
|
requireOption(options, "pcr");
|
|
256
|
+
if (["topic", "pointer", "page", "page-size"].some((key) => options[key] !== undefined)) {
|
|
257
|
+
validatePointerOptions(options, "topic");
|
|
258
|
+
const report = selectGuidance({ root, pcrId: String(options.pcr), topic: options.topic, pointer: options.pointer, ...consumptionPage(options) });
|
|
259
|
+
return consumptionOutput(withConsumptionContinuation(report, "guidance", options), options);
|
|
260
|
+
}
|
|
261
|
+
if (options.output) return consumptionOutput(buildGuidance({ root, pcrId: String(options.pcr) }), options);
|
|
237
262
|
return ok(writeOutput(buildGuidance({ root, pcrId: String(options.pcr) }), format, JSON.stringify));
|
|
238
263
|
}
|
|
264
|
+
if (command === "inspect") {
|
|
265
|
+
requireOption(options, "input");
|
|
266
|
+
validatePointerOptions(options, "section");
|
|
267
|
+
if (options.pointer === undefined && (!options.section || options.section === "summary") && (options.page || options["page-size"])) {
|
|
268
|
+
throw new CliError("PCR_CLI_OPTION_CONFLICT", "Summary is not paginated. Choose --section references|exchanges|instances|documents for paging.");
|
|
269
|
+
}
|
|
270
|
+
const report = inspectTidas({ input: options.input, related: options.related, section: options.section, pointer: options.pointer, ...consumptionPage(options) });
|
|
271
|
+
return consumptionOutput(withConsumptionContinuation(report, "inspect", options), options);
|
|
272
|
+
}
|
|
273
|
+
if (command === "calculate") {
|
|
274
|
+
requireOption(options, "input");
|
|
275
|
+
const source = readJsonDocument(options.input);
|
|
276
|
+
return consumptionOutput({ ...calculate(source.value), request_source: { file: source.file, sha256: source.sha256 } }, options);
|
|
277
|
+
}
|
|
278
|
+
if (command === "review") {
|
|
279
|
+
if (!positional.length) throw new CliError("PCR_CLI_MISSING_SUBCOMMAND", "Use review prepare to create an Agent draft or review check to verify its envelope. Run review --help.");
|
|
280
|
+
requireOption(options, "input");
|
|
281
|
+
requireOption(options, "pcr");
|
|
282
|
+
const args = { root, pcrId: String(options.pcr), input: options.input, related: options.related };
|
|
283
|
+
if (positional[0] === "prepare") return consumptionOutput(prepareReview(args), options);
|
|
284
|
+
requireOption(options, "report");
|
|
285
|
+
const source = readJsonDocument(options.report);
|
|
286
|
+
const report = checkReview({ ...args, report: source.value });
|
|
287
|
+
return consumptionOutput({ ...report, report_source: { file: source.file, sha256: source.sha256 } }, options, report.envelope_valid ? 0 : 2);
|
|
288
|
+
}
|
|
239
289
|
if (command === "validate-model") {
|
|
240
290
|
requireOption(options, "pcr");
|
|
241
291
|
requireOption(options, "input");
|
|
@@ -297,6 +347,48 @@ function ok(stdout, exitCode = 0) {
|
|
|
297
347
|
return { stdout, stderr: "", exitCode };
|
|
298
348
|
}
|
|
299
349
|
|
|
350
|
+
function consumptionPage(options) {
|
|
351
|
+
return {
|
|
352
|
+
page: positiveIntegerOption(options.page, "page", 1),
|
|
353
|
+
pageSize: positiveIntegerOption(options["page-size"], "page-size", 10, 100),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validatePointerOptions(options, selector) {
|
|
358
|
+
if (options.pointer !== undefined && [selector, "page", "page-size"].some((key) => options[key] !== undefined)) {
|
|
359
|
+
throw new CliError("PCR_CLI_OPTION_CONFLICT", `Use --pointer alone, or --${selector} with optional pagination.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function withConsumptionContinuation(report, command, options) {
|
|
364
|
+
const parts = command === "guidance" ? commandPrefix(options) : ["tiangong-pcr"];
|
|
365
|
+
parts.push(command);
|
|
366
|
+
if (command === "guidance") parts.push(...sourceArguments(options));
|
|
367
|
+
for (const key of command === "guidance" ? ["pcr", "topic"] : ["input", "related", "section"]) {
|
|
368
|
+
if (options[key] !== undefined) parts.push(`--${key}`, shellToken(["input", "related"].includes(key) ? path.resolve(options[key]) : options[key]));
|
|
369
|
+
}
|
|
370
|
+
const pagination = report.pagination;
|
|
371
|
+
return {
|
|
372
|
+
...report,
|
|
373
|
+
next_command: pagination?.has_more ? `${parts.join(" ")} --page ${pagination.page + 1} --page-size ${pagination.page_size} --format json` : null,
|
|
374
|
+
next_step: command === "guidance"
|
|
375
|
+
? "Read relevant topics; use --pointer with an item's source.pointer for complete text. Judge applicability and cite the returned source."
|
|
376
|
+
: "Inspect paged exchanges/instances and references; use --pointer with source.pointer for full fields. Resolve evidence gaps before methodological conclusions.",
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function consumptionOutput(report, options, exitCode = 0) {
|
|
381
|
+
const output = `${JSON.stringify(report, null, 2)}\n`;
|
|
382
|
+
if (options.output) {
|
|
383
|
+
const filename = path.resolve(String(options.output));
|
|
384
|
+
try { writeFileSync(filename, output, { encoding: "utf8", flag: "wx" }); }
|
|
385
|
+
catch (error) { throw new CliError("PCR_CLI_OUTPUT_WRITE", `Cannot create new output file ${filename}: ${error.message}. Choose an unused path in an existing directory.`); }
|
|
386
|
+
return ok(`${JSON.stringify({ artifact: filename, sha256: sha256(output), bytes: Buffer.byteLength(output), kind: report.report_kind ?? report.check_kind ?? report.guidance_kind ?? report.inspection_kind ?? report.calculation_kind, ...(report.envelope_valid === undefined ? {} : { envelope_valid: report.envelope_valid, methodology_approval: false }), next_step: "Read the saved artifact. Source inputs were not modified." }, null, 2)}\n`, exitCode);
|
|
387
|
+
}
|
|
388
|
+
if (output.length > 32768) throw new CliError("PCR_CLI_OUTPUT_LARGE", "Result exceeds 32768 characters. Use --output <new-file> for the complete artifact or request a smaller page/pointer.");
|
|
389
|
+
return ok(output, exitCode);
|
|
390
|
+
}
|
|
391
|
+
|
|
300
392
|
function fail(error, requestedFormat, requestedRoot, selected = null) {
|
|
301
393
|
const normalized = normalizeError(error);
|
|
302
394
|
if (
|
|
@@ -1349,6 +1441,10 @@ This command rejects scaffold, incomplete, deprecated, or otherwise unusable PCR
|
|
|
1349
1441
|
|
|
1350
1442
|
Options:
|
|
1351
1443
|
--pcr <pcr-id> PCR id.
|
|
1444
|
+
--topic <topic> Bounded, cited selection (default overview when paging).
|
|
1445
|
+
--pointer <JSON Pointer> Read a complete value from the verified structured projection.
|
|
1446
|
+
--page <n> --page-size <n> Topic pagination; default 10, maximum 100 items.
|
|
1447
|
+
--output <new-file> Save complete JSON without overwriting an existing file.
|
|
1352
1448
|
--format json Output format. JSON is recommended for Agents.
|
|
1353
1449
|
--root <path> PCR repository root.
|
|
1354
1450
|
--library <file> Local library.sqlite (or PCR_LIBRARY). Mutually exclusive with --root.
|
|
@@ -1358,16 +1454,119 @@ Options:
|
|
|
1358
1454
|
JSON output includes:
|
|
1359
1455
|
pcr, readiness, system_boundary, reference_flow, measurement_rules, process_map,
|
|
1360
1456
|
process_inventory, allocation_rules, validation_rules, data_sources, and validation_notes.
|
|
1457
|
+
Without selection flags, the legacy full guidance shape is preserved.
|
|
1458
|
+
Selected guidance includes source pointers, content hashes, readiness and pagination.
|
|
1459
|
+
|
|
1460
|
+
Topics:
|
|
1461
|
+
${GUIDANCE_TOPICS.join(", ")}
|
|
1462
|
+
|
|
1463
|
+
Example:
|
|
1464
|
+
tiangong-pcr guidance --pcr <pcr-id> --topic boundary --format json
|
|
1465
|
+
tiangong-pcr guidance --pcr <pcr-id> --pointer /system_boundary/rules/0 --format json
|
|
1361
1466
|
|
|
1362
1467
|
Agent next step:
|
|
1363
|
-
|
|
1364
|
-
|
|
1468
|
+
Choose general LCA authoring, optional TIDAS authoring or existing-data review.
|
|
1469
|
+
Judge applicability and declared scope. Cite returned source references; expand truncated items with --pointer.
|
|
1470
|
+
`;
|
|
1471
|
+
}
|
|
1472
|
+
if (definition.key === "inspect") {
|
|
1473
|
+
return `Usage: tiangong-pcr inspect --input <native-tidas.json> [options]
|
|
1474
|
+
|
|
1475
|
+
Read local TIDAS process/model/support datasets without changing them or fetching references.
|
|
1476
|
+
This is fact inspection, not TIDAS schema validation or PCR compliance. No PCR library is needed.
|
|
1477
|
+
|
|
1478
|
+
Options:
|
|
1479
|
+
--input <file> One native TIDAS JSON dataset; wrapped rows are unsupported.
|
|
1480
|
+
--related <directory> Explicit local package: at most 200 JSON files / 64 MiB total.
|
|
1481
|
+
--section <section> ${INSPECTION_SECTIONS.join("|")}; default summary.
|
|
1482
|
+
--pointer <JSON Pointer> Read a full original value; mutually exclusive with section/paging.
|
|
1483
|
+
--page <n> --page-size <n> Paged sections: default 10, maximum 100 items.
|
|
1484
|
+
--output <new-file> Save complete JSON; existing files are never overwritten.
|
|
1485
|
+
--format json --help JSON output or this help.
|
|
1486
|
+
|
|
1487
|
+
Output:
|
|
1488
|
+
Original file/hash/pointer evidence, explicitly partial scope, local reference statuses and next_command.
|
|
1489
|
+
Previews may be truncated; --pointer retrieves the complete value, --output saves large results.
|
|
1490
|
+
References match kind, UUID and requested version; duplicate matches are ambiguous.
|
|
1491
|
+
Missing/unsupported identities remain unresolved. Unspecified versions are disclosed.
|
|
1492
|
+
Related JSON without a dataset root is ignored explicitly; malformed JSON and symlinks fail.
|
|
1493
|
+
|
|
1494
|
+
Examples:
|
|
1495
|
+
tiangong-pcr inspect --input wheat.process.json --related ./datasets --section exchanges --format json
|
|
1496
|
+
tiangong-pcr inspect --input wheat.model.json --related ./datasets --section references --format json
|
|
1497
|
+
|
|
1498
|
+
Next:
|
|
1499
|
+
Inspect scope and reference basis, resolve missing evidence, select PCR guidance and use review prepare.
|
|
1500
|
+
Unit interpretation needs local flow, flow-property and unit-group evidence. Use the TIDAS toolkit for schema validation.
|
|
1501
|
+
`;
|
|
1502
|
+
}
|
|
1503
|
+
if (definition.key === "calculate") {
|
|
1504
|
+
return `Usage: tiangong-pcr calculate --input <calculation.json> [--output <new-file>] [--format json]
|
|
1505
|
+
|
|
1506
|
+
Perform reproducible arithmetic using explicit values, units, basis and evidence. No library is needed.
|
|
1507
|
+
All requests require operation, basis and evidence. Numeric values may be finite numbers or decimal strings.
|
|
1508
|
+
|
|
1509
|
+
Normalization example:
|
|
1510
|
+
{"operation":"normalize","amount":50,"unit":"kg N","source_reference":1000,
|
|
1511
|
+
"target_reference":1,"reference_unit":"kg grain","basis":"same moisture and gate",
|
|
1512
|
+
"evidence":"production record A; explicit source and target reference amounts"}
|
|
1513
|
+
|
|
1514
|
+
Conversion fields:
|
|
1515
|
+
operation: convert; amount, from_unit, to_unit, factor, basis, evidence.
|
|
1516
|
+
factor is supplied in to_unit / from_unit; no unit equivalence or factor is inferred.
|
|
1517
|
+
|
|
1518
|
+
Balance fields:
|
|
1519
|
+
operation: balance; inputs and outputs: [{"amount":1,"label":"record A"}],
|
|
1520
|
+
accumulation (explicit, even if zero), unit, basis, evidence.
|
|
1521
|
+
All terms must use the same physical basis and unit. Residual = inputs - outputs - accumulation.
|
|
1522
|
+
|
|
1523
|
+
Output:
|
|
1524
|
+
Request hash, supplied request, formula, result and interpretation limits; floating-point arithmetic.
|
|
1525
|
+
No threshold, physical validity or anomaly verdict is inferred. Missing values are not zero.
|
|
1526
|
+
Next: retain the calculation artifact and assess its applicability and tolerance in the Agent review.
|
|
1527
|
+
`;
|
|
1528
|
+
}
|
|
1529
|
+
if (definition.command === "review") {
|
|
1530
|
+
return `Usage:
|
|
1531
|
+
tiangong-pcr review prepare --pcr <pcr-id> --input <native-tidas.json> [--related <directory>] --output <new-draft.json>
|
|
1532
|
+
tiangong-pcr review check --pcr <pcr-id> --input <native-tidas.json> [--related <directory>] --report <review.json>
|
|
1533
|
+
|
|
1534
|
+
prepare creates an unreviewed Agent artifact with exact input/PCR bindings, scope and uncovered topics.
|
|
1535
|
+
The Agent investigates and edits that artifact. check verifies its envelope and evidence locations.
|
|
1536
|
+
Neither command runs an LLM, validates TIDAS schemas or approves methodology.
|
|
1537
|
+
|
|
1538
|
+
Options:
|
|
1539
|
+
--pcr <pcr-id> --input <file> Explicit PCR and native TIDAS input, unchanged throughout review.
|
|
1540
|
+
--related <directory> Same explicitly supplied local package used during inspection.
|
|
1541
|
+
--report <file> Agent review JSON, required only by check.
|
|
1542
|
+
--output <new-file> Save complete output without replacing an existing file.
|
|
1543
|
+
--root <path> Repository source, mutually exclusive with --library.
|
|
1544
|
+
--library <file> Local snapshot or PCR_LIBRARY / installed library.
|
|
1545
|
+
--library-sha256 <sha256:hex> Verify and pin that snapshot.
|
|
1546
|
+
--format json --help JSON output or this help.
|
|
1547
|
+
|
|
1548
|
+
Agent report:
|
|
1549
|
+
status: draft|reviewed; scope: description, applicability, limitations.
|
|
1550
|
+
coverage: [{topic, status: reviewed|not_applicable|not_reviewed, rationale}].
|
|
1551
|
+
findings: [{id, kind: confirmed_issue|suspected_anomaly|evidence_gap, severity: error|warning|info,
|
|
1552
|
+
observation, rationale, input_refs, pcr_refs, suggested_action, questions, alternative_explanations?}].
|
|
1553
|
+
Copy input_refs from inspect source objects and pcr_refs from selected guidance source objects.
|
|
1554
|
+
Cite the nearest existing parent for a missing field. pcr_refs may be empty for purely structural observations.
|
|
1555
|
+
Preserve free reasoning and unresolved questions; do not infer missing lifecycle stages from a partial process.
|
|
1556
|
+
|
|
1557
|
+
Exit codes:
|
|
1558
|
+
0 preparation completed or report envelope valid (drafts may be valid);
|
|
1559
|
+
1 usage/input/runtime error; 2 report shape, binding or evidence-location problem.
|
|
1560
|
+
envelope_valid and methodology_approval=false are separate from Agent findings and severity.
|
|
1561
|
+
|
|
1562
|
+
Next: use the Skill review reference, investigate applicable topics and share findings with their scope limits.
|
|
1365
1563
|
`;
|
|
1366
1564
|
}
|
|
1367
1565
|
if (definition.key === "validate-model") {
|
|
1368
1566
|
return `Usage: tiangong-pcr validate-model --pcr <pcr-id> --input <file> [options]
|
|
1369
1567
|
|
|
1370
|
-
|
|
1568
|
+
Legacy diagnostic: check whether PCR reference qualifiers occur in the input text.
|
|
1569
|
+
JSON is treated as text; this does not parse TIDAS fields or perform semantic review.
|
|
1371
1570
|
|
|
1372
1571
|
Options:
|
|
1373
1572
|
--pcr <pcr-id> PCR id.
|
|
@@ -1387,14 +1586,15 @@ Exit codes:
|
|
|
1387
1586
|
1 usage, input, or runtime error; 2 validation is inconclusive or the --fail-on threshold was reached.
|
|
1388
1587
|
|
|
1389
1588
|
Agent next step:
|
|
1390
|
-
|
|
1589
|
+
Treat matches as text-presence evidence only. Use inspect and review prepare/check for Agent-led TIDAS review.
|
|
1391
1590
|
`;
|
|
1392
1591
|
}
|
|
1393
1592
|
if (definition.key === "validate-dataset") {
|
|
1394
1593
|
return `Usage: tiangong-pcr validate-dataset --pcr <pcr-id> --input <file> [options]
|
|
1395
1594
|
|
|
1396
1595
|
Check a foreground data package JSON object against the implemented subset of selected PCR guidance.
|
|
1397
|
-
|
|
1596
|
+
Currently checks collection protocol ID presence, not values, units, evidence quality or TIDAS validity.
|
|
1597
|
+
This optional legacy package format is not required for general LCA authoring or native TIDAS review.
|
|
1398
1598
|
|
|
1399
1599
|
Options:
|
|
1400
1600
|
--pcr <pcr-id> PCR id.
|
|
@@ -1468,7 +1668,11 @@ Commands:
|
|
|
1468
1668
|
coverage list --classification <system>:<version> [--status <coverage-status>] [--page <n>] [--page-size <n>] [--format json|table]
|
|
1469
1669
|
resolve (--classification <system>:<version>:<code> | --pcr <pcr-id>) [--format json]
|
|
1470
1670
|
show --pcr <pcr-id> [--lang en-US|zh-CN]
|
|
1471
|
-
guidance --pcr <pcr-id> [--format json]
|
|
1671
|
+
guidance --pcr <pcr-id> [--topic <topic> | --pointer <pointer>] [--output <new-file>] [--format json]
|
|
1672
|
+
inspect --input <native-tidas.json> [--related <directory>] [--section <section> | --pointer <pointer>]
|
|
1673
|
+
calculate --input <calculation.json> [--output <new-file>]
|
|
1674
|
+
review prepare --pcr <pcr-id> --input <native-tidas.json> [--related <directory>] --output <new-draft.json>
|
|
1675
|
+
review check --pcr <pcr-id> --input <native-tidas.json> [--related <directory>] --report <review.json>
|
|
1472
1676
|
validate-model --pcr <pcr-id> --input <file> [--format json] [--fail-on never|error|warning]
|
|
1473
1677
|
validate-dataset --pcr <pcr-id> --input <file> [--format json] [--fail-on never|error|warning]
|
|
1474
1678
|
feedback draft --type <type> [--pcr <pcr-id>] [--summary <text>]
|
|
@@ -1479,8 +1683,9 @@ Agent workflow:
|
|
|
1479
1683
|
If a PCR id is available, run resolve --pcr <pcr-id> --format json. Supply exactly one selector.
|
|
1480
1684
|
3. If no code is available, use tree/list to browse explicit material PCR hierarchy. list defaults to 10 records per page.
|
|
1481
1685
|
4. Check resolution_status and readiness. Run guidance only for mapped material PCRs when usable_for_guidance is true.
|
|
1482
|
-
5.
|
|
1483
|
-
6.
|
|
1686
|
+
5. Guide general LCA data authoring, optionally map a draft into TIDAS, or inspect existing TIDAS inputs.
|
|
1687
|
+
6. For review, investigate applicable guidance and evidence, fill review prepare's draft, then use review check.
|
|
1688
|
+
CLI checks do not approve methodology. Existing validate-* commands are limited legacy diagnostics.
|
|
1484
1689
|
7. If PCR guidance is missing or ambiguous, run feedback draft with the observed gap.
|
|
1485
1690
|
`;
|
|
1486
1691
|
}
|