@disclosureos/cli 1.0.0 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/README.md +122 -3
- package/dist/index.js +3677 -262
- package/package.json +9 -7
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
-
import { dirname, join } from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/utils/args.ts
|
|
9
9
|
var SHORT_FLAG_MAP = {
|
|
@@ -158,8 +158,8 @@ var CLASSIFICATION_TEMPLATE = [
|
|
|
158
158
|
evidenceRefs: []
|
|
159
159
|
}
|
|
160
160
|
];
|
|
161
|
-
function getTemplate(
|
|
162
|
-
const data =
|
|
161
|
+
function getTemplate(name2) {
|
|
162
|
+
const data = name2 === "observation" ? buildObservation(false) : name2 === "observation-full" ? buildObservation(true) : name2 === "assessment" ? ASSESSMENT_TEMPLATE : CLASSIFICATION_TEMPLATE;
|
|
163
163
|
return JSON.stringify(data, null, 2);
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -177,8 +177,8 @@ function writeOutput(filePath, data) {
|
|
|
177
177
|
function findJSONFiles(dirPath, recursive) {
|
|
178
178
|
const resolved = resolve(dirPath);
|
|
179
179
|
if (!existsSync(resolved)) return [];
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
180
|
+
const stat2 = statSync(resolved);
|
|
181
|
+
if (stat2.isFile()) {
|
|
182
182
|
return extname(resolved) === ".json" ? [resolved] : [];
|
|
183
183
|
}
|
|
184
184
|
const results = [];
|
|
@@ -259,8 +259,8 @@ function resolveTemplateName(args2) {
|
|
|
259
259
|
console.log(error(`Unknown template: "${sub}"`));
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
262
|
-
function getTypeScriptTemplate(
|
|
263
|
-
switch (
|
|
262
|
+
function getTypeScriptTemplate(name2) {
|
|
263
|
+
switch (name2) {
|
|
264
264
|
case "observation":
|
|
265
265
|
return TS_OBSERVATION;
|
|
266
266
|
case "observation-full":
|
|
@@ -395,7 +395,7 @@ Generate typed templates for DisclosureOS data structures.
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// src/commands/validate.ts
|
|
398
|
-
import { parseEvidenceRef } from "@disclosureos/records/shared";
|
|
398
|
+
import { parseEvidenceRef, isSensorRef } from "@disclosureos/records/shared";
|
|
399
399
|
import { parseEnrichedObservation } from "@disclosureos/schema";
|
|
400
400
|
function validate(args2) {
|
|
401
401
|
if (args2.flags["help"]) {
|
|
@@ -413,10 +413,10 @@ function validate(args2) {
|
|
|
413
413
|
const seen = /* @__PURE__ */ new Set();
|
|
414
414
|
const files = [];
|
|
415
415
|
const emptyTargets = [];
|
|
416
|
-
for (const
|
|
417
|
-
const found = findJSONFiles(
|
|
416
|
+
for (const target2 of targets) {
|
|
417
|
+
const found = findJSONFiles(target2, recursive);
|
|
418
418
|
if (found.length === 0) {
|
|
419
|
-
emptyTargets.push(
|
|
419
|
+
emptyTargets.push(target2);
|
|
420
420
|
continue;
|
|
421
421
|
}
|
|
422
422
|
for (const f of found) {
|
|
@@ -503,7 +503,7 @@ function validateFileStructured(filePath, strict) {
|
|
|
503
503
|
const obs = data;
|
|
504
504
|
const structuralErrors = parseEnrichedObservation(obs).issues;
|
|
505
505
|
const semanticErrors = validateSemantics(obs);
|
|
506
|
-
const warnings = [...validateEvidenceRefs(obs), ...strict ? validateStrict(obs) : []];
|
|
506
|
+
const warnings = [...validateEvidenceRefs(obs), ...validateSensorRefs(obs), ...strict ? validateStrict(obs) : []];
|
|
507
507
|
const allErrors = [...structuralErrors, ...semanticErrors];
|
|
508
508
|
return {
|
|
509
509
|
file: filePath,
|
|
@@ -527,7 +527,7 @@ function validateFile(filePath, strict) {
|
|
|
527
527
|
const obs = data;
|
|
528
528
|
const structuralErrors = parseEnrichedObservation(obs).issues;
|
|
529
529
|
const semanticErrors = validateSemantics(obs);
|
|
530
|
-
const warnings = [...validateEvidenceRefs(obs), ...strict ? validateStrict(obs) : []];
|
|
530
|
+
const warnings = [...validateEvidenceRefs(obs), ...validateSensorRefs(obs), ...strict ? validateStrict(obs) : []];
|
|
531
531
|
const allErrors = [...structuralErrors, ...semanticErrors];
|
|
532
532
|
if (allErrors.length > 0 || warnings.length > 0) {
|
|
533
533
|
console.log(`
|
|
@@ -628,6 +628,25 @@ function validateEvidenceRefs(obs) {
|
|
|
628
628
|
}
|
|
629
629
|
return warnings;
|
|
630
630
|
}
|
|
631
|
+
function validateSensorRefs(obs) {
|
|
632
|
+
const warnings = [];
|
|
633
|
+
const sensorEvidence = obs["sensorEvidence"];
|
|
634
|
+
if (!sensorEvidence || typeof sensorEvidence !== "object") return warnings;
|
|
635
|
+
const sensors = sensorEvidence["sensors"];
|
|
636
|
+
if (!Array.isArray(sensors)) return warnings;
|
|
637
|
+
sensors.forEach((sensor, i) => {
|
|
638
|
+
if (!sensor || typeof sensor !== "object") return;
|
|
639
|
+
const ref = sensor["sensorRef"];
|
|
640
|
+
if (ref === void 0) return;
|
|
641
|
+
if (typeof ref !== "string" || !isSensorRef(ref)) {
|
|
642
|
+
warnings.push({
|
|
643
|
+
path: `sensorEvidence.sensors[${i}].sensorRef`,
|
|
644
|
+
message: `malformed sensor ref ${JSON.stringify(ref)} (expected "<org-slug>:<sensor-id>", e.g. "eldaeon:dionysus-passive-radar")`
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
return warnings;
|
|
649
|
+
}
|
|
631
650
|
function validateStrict(obs) {
|
|
632
651
|
const warnings = [];
|
|
633
652
|
const recommended = ["summary", "description", "objectCharacteristics", "sourceData", "witnesses"];
|
|
@@ -679,6 +698,7 @@ Validate observation JSON files against the full schema.
|
|
|
679
698
|
console.log(` \u2022 OCS node IDs in origin classifications (if present)`);
|
|
680
699
|
console.log(` \u2022 Unknown top-level keys rejected (third-party data belongs under "extensions")`);
|
|
681
700
|
console.log(` \u2022 Evidence refs on claims resolve to in-record evidence (warns on dangling)`);
|
|
701
|
+
console.log(` \u2022 Sensor refs follow the "<org-slug>:<sensor-id>" manifest convention (warns on malformed)`);
|
|
682
702
|
console.log(` \u2022 Semantic sanity (non-negative duration, speed, counts)
|
|
683
703
|
`);
|
|
684
704
|
console.log(`${dim("Examples:")}`);
|
|
@@ -690,306 +710,3675 @@ Validate observation JSON files against the full schema.
|
|
|
690
710
|
`);
|
|
691
711
|
}
|
|
692
712
|
|
|
693
|
-
// src/commands/
|
|
694
|
-
import {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (args2.flags["help"]) {
|
|
713
|
+
// src/commands/manifest.ts
|
|
714
|
+
import { validateSensorManifest } from "@disclosureos/instruments";
|
|
715
|
+
function manifest(args2) {
|
|
716
|
+
if (args2.flags["help"] || args2.subcommand !== "validate") {
|
|
698
717
|
printUsage3();
|
|
718
|
+
if (!args2.flags["help"] && args2.subcommand !== void 0 && args2.subcommand !== "validate") {
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
699
721
|
return;
|
|
700
722
|
}
|
|
701
|
-
const targets =
|
|
723
|
+
const targets = args2.positional.filter((t) => !!t);
|
|
702
724
|
if (targets.length === 0) {
|
|
703
725
|
printUsage3();
|
|
704
726
|
return;
|
|
705
727
|
}
|
|
706
728
|
const recursive = !!args2.flags["recursive"];
|
|
707
729
|
const jsonMode = !!args2.flags["json"];
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const found = findJSONFiles(target, recursive);
|
|
713
|
-
if (found.length === 0) {
|
|
714
|
-
emptyTargets.push(target);
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
for (const f of found) {
|
|
718
|
-
if (!seen.has(f)) {
|
|
719
|
-
seen.add(f);
|
|
720
|
-
files.push(f);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
730
|
+
const files = [...new Set(targets.flatMap((target2) => findJSONFiles(target2, recursive)))];
|
|
731
|
+
if (files.length === 0) {
|
|
732
|
+
console.log(error(`No JSON files found at: ${targets.join(", ")}`));
|
|
733
|
+
process.exit(1);
|
|
723
734
|
}
|
|
735
|
+
const results = files.map(validateManifestFile);
|
|
724
736
|
if (jsonMode) {
|
|
725
|
-
|
|
737
|
+
const valid = results.every((result) => result.valid);
|
|
738
|
+
console.log(JSON.stringify({ files: results, valid }, null, 2));
|
|
739
|
+
if (!valid) process.exit(1);
|
|
726
740
|
return;
|
|
727
741
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
if (files.length === 0) {
|
|
732
|
-
process.exit(1);
|
|
733
|
-
}
|
|
734
|
-
let hasErrors = false;
|
|
735
|
-
const percentages = [];
|
|
736
|
-
const reqPercentages = [];
|
|
737
|
-
for (const file of files) {
|
|
738
|
-
const result = scoreFile(file);
|
|
742
|
+
let failed = false;
|
|
743
|
+
for (const result of results) {
|
|
739
744
|
if (!result.valid) {
|
|
740
|
-
|
|
745
|
+
failed = true;
|
|
746
|
+
console.log(`
|
|
747
|
+
${error(`${result.file} INVALID`)}`);
|
|
748
|
+
for (const issue of result.errors) {
|
|
749
|
+
console.log(` ${error(`${issue.path}: ${issue.message}`)}`);
|
|
750
|
+
}
|
|
741
751
|
continue;
|
|
742
752
|
}
|
|
743
|
-
|
|
744
|
-
|
|
753
|
+
const summary = result.summary;
|
|
754
|
+
console.log(`
|
|
755
|
+
${success(`${result.file}`)}`);
|
|
756
|
+
console.log(` ${dim(formatManifestLine(summary))}`);
|
|
757
|
+
console.log(` ${dim("by modality:")} ${Object.entries(summary.byModality).map(([m, n]) => `${m} ${n}`).join(", ")}`);
|
|
758
|
+
if (summary.proposedSensorTypes.length > 0) {
|
|
759
|
+
console.log(` ${dim(`proposed sensor types (${summary.proposedSensorTypes.length}):`)} ${summary.proposedSensorTypes.join(", ")}`);
|
|
760
|
+
}
|
|
761
|
+
if (summary.proposedDetectionMethods.length > 0) {
|
|
762
|
+
console.log(` ${dim(`proposed detection methods (${summary.proposedDetectionMethods.length}):`)} ${summary.proposedDetectionMethods.join(", ")}`);
|
|
763
|
+
}
|
|
745
764
|
}
|
|
746
765
|
console.log("");
|
|
747
|
-
if (
|
|
748
|
-
console.log(error(
|
|
766
|
+
if (failed) {
|
|
767
|
+
console.log(error(`Validation failed`));
|
|
749
768
|
process.exit(1);
|
|
750
769
|
}
|
|
751
|
-
|
|
752
|
-
const reqMean = avg(reqPercentages);
|
|
753
|
-
console.log(
|
|
754
|
-
success(
|
|
755
|
-
`${files.length} file(s) scored \u2014 mean completeness ${mean}%, required fields ${reqMean}%`
|
|
756
|
-
)
|
|
757
|
-
);
|
|
770
|
+
console.log(success(`All ${files.length} manifest(s) valid`));
|
|
758
771
|
}
|
|
759
|
-
function
|
|
760
|
-
return
|
|
772
|
+
function formatManifestLine(summary) {
|
|
773
|
+
return `${summary.org} (${summary.orgSlug}) \u2014 ${summary.sensors} sensors, ${summary.futureUpgrades} future upgrades`;
|
|
761
774
|
}
|
|
762
|
-
function
|
|
775
|
+
function validateManifestFile(file) {
|
|
763
776
|
let data;
|
|
764
777
|
try {
|
|
765
|
-
data = readJSON(
|
|
778
|
+
data = readJSON(file);
|
|
766
779
|
} catch {
|
|
767
|
-
|
|
768
|
-
return emptyResult(filePath, "Failed to parse JSON");
|
|
769
|
-
}
|
|
770
|
-
if (!data || typeof data !== "object") {
|
|
771
|
-
console.log(error(`${filePath}: not a valid JSON object`));
|
|
772
|
-
return emptyResult(filePath, "Not a valid JSON object");
|
|
780
|
+
return { file, valid: false, errors: [{ path: "", message: "Failed to parse JSON" }] };
|
|
773
781
|
}
|
|
774
|
-
const
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
for (const issue of parsed.issues.slice(0, 5)) {
|
|
778
|
-
console.log(` ${dim(`${issue.path}: ${issue.message}`)}`);
|
|
779
|
-
}
|
|
780
|
-
return emptyResult(filePath, "Invalid observation");
|
|
782
|
+
const issues = validateSensorManifest(data);
|
|
783
|
+
if (issues.length > 0) {
|
|
784
|
+
return { file, valid: false, errors: issues };
|
|
781
785
|
}
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
for (const path of shown) {
|
|
791
|
-
console.log(` ${dim("\u2022")} ${path}`);
|
|
786
|
+
const parsed = data;
|
|
787
|
+
const byModality = {};
|
|
788
|
+
const proposedSensorTypes = [];
|
|
789
|
+
const proposedDetectionMethods = [];
|
|
790
|
+
for (const sensor of parsed.sensors) {
|
|
791
|
+
byModality[sensor.modality] = (byModality[sensor.modality] ?? 0) + 1;
|
|
792
|
+
if (sensor.recordsMapping?.proposedSensorType && sensor.recordsMapping.sensorType) {
|
|
793
|
+
proposedSensorTypes.push(sensor.recordsMapping.sensorType);
|
|
792
794
|
}
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
+
if (sensor.recordsMapping?.proposedDetectionMethod && sensor.recordsMapping.detectionMethod) {
|
|
796
|
+
proposedDetectionMethods.push(sensor.recordsMapping.detectionMethod);
|
|
795
797
|
}
|
|
796
798
|
}
|
|
797
|
-
return {
|
|
798
|
-
file: filePath,
|
|
799
|
-
valid: true,
|
|
800
|
-
percentage: comp.percentage,
|
|
801
|
-
requiredPercentage: comp.requiredPercentage,
|
|
802
|
-
present: comp.present,
|
|
803
|
-
total: comp.total,
|
|
804
|
-
missing: comp.missing
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
function emptyResult(file, errorMsg) {
|
|
808
799
|
return {
|
|
809
800
|
file,
|
|
810
|
-
valid: false,
|
|
811
|
-
percentage: 0,
|
|
812
|
-
requiredPercentage: 0,
|
|
813
|
-
present: 0,
|
|
814
|
-
total: 0,
|
|
815
|
-
missing: [],
|
|
816
|
-
error: errorMsg
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
function runJsonMode2(files, emptyTargets) {
|
|
820
|
-
const fileResults = [];
|
|
821
|
-
for (const file of files) {
|
|
822
|
-
fileResults.push(scoreFileQuiet(file));
|
|
823
|
-
}
|
|
824
|
-
const validResults = fileResults.filter((r) => r.valid);
|
|
825
|
-
const meanPercentage = avg(validResults.map((r) => r.percentage));
|
|
826
|
-
const meanRequired = avg(validResults.map((r) => r.requiredPercentage));
|
|
827
|
-
const allValid = fileResults.every((r) => r.valid) && emptyTargets.length === 0;
|
|
828
|
-
const output = {
|
|
829
|
-
files: fileResults,
|
|
830
|
-
emptyTargets,
|
|
831
|
-
totals: {
|
|
832
|
-
files: files.length,
|
|
833
|
-
meanPercentage,
|
|
834
|
-
meanRequiredPercentage: meanRequired,
|
|
835
|
-
emptyTargets: emptyTargets.length
|
|
836
|
-
},
|
|
837
|
-
valid: allValid
|
|
838
|
-
};
|
|
839
|
-
console.log(JSON.stringify(output, null, 2));
|
|
840
|
-
if (!allValid) {
|
|
841
|
-
process.exit(1);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
function scoreFileQuiet(filePath) {
|
|
845
|
-
let data;
|
|
846
|
-
try {
|
|
847
|
-
data = readJSON(filePath);
|
|
848
|
-
} catch {
|
|
849
|
-
return emptyResult(filePath, "Failed to parse JSON");
|
|
850
|
-
}
|
|
851
|
-
if (!data || typeof data !== "object") {
|
|
852
|
-
return emptyResult(filePath, "Not a valid JSON object");
|
|
853
|
-
}
|
|
854
|
-
const parsed = parseEnrichedObservation2(data);
|
|
855
|
-
if (!parsed.success) {
|
|
856
|
-
return emptyResult(filePath, `Invalid observation (${parsed.issues.length} issue${parsed.issues.length !== 1 ? "s" : ""})`);
|
|
857
|
-
}
|
|
858
|
-
const comp = getCompleteness(data);
|
|
859
|
-
return {
|
|
860
|
-
file: filePath,
|
|
861
801
|
valid: true,
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
802
|
+
errors: [],
|
|
803
|
+
summary: {
|
|
804
|
+
org: parsed.org,
|
|
805
|
+
orgSlug: parsed.orgSlug,
|
|
806
|
+
sensors: parsed.sensors.length,
|
|
807
|
+
byModality,
|
|
808
|
+
proposedSensorTypes,
|
|
809
|
+
proposedDetectionMethods,
|
|
810
|
+
futureUpgrades: parsed.futureUpgrades?.length ?? 0
|
|
811
|
+
}
|
|
867
812
|
};
|
|
868
813
|
}
|
|
869
814
|
function printUsage3() {
|
|
870
|
-
console.log(heading(`${BRAND}
|
|
815
|
+
console.log(heading(`${BRAND} manifest`));
|
|
871
816
|
console.log(`
|
|
872
|
-
|
|
817
|
+
Validate sensor-manifest JSON files (@disclosureos/instruments).
|
|
873
818
|
`);
|
|
874
819
|
console.log(`${dim("Usage:")}`);
|
|
875
|
-
console.log(` disclosureos
|
|
876
|
-
`);
|
|
877
|
-
console.log(`${dim("Paths:")}`);
|
|
878
|
-
console.log(` Accepts one or more files, directories, or shell globs.
|
|
820
|
+
console.log(` disclosureos manifest validate <path...> [options]
|
|
879
821
|
`);
|
|
880
822
|
console.log(`${dim("Options:")}`);
|
|
881
|
-
console.log(` --recursive, -r
|
|
823
|
+
console.log(` --recursive, -r Validate all JSON files in directories recursively`);
|
|
882
824
|
console.log(` --json, -j Output results as structured JSON (for CI and scripts)`);
|
|
883
825
|
console.log(` --help, -h Show this help message
|
|
884
826
|
`);
|
|
885
|
-
console.log(`${dim("
|
|
886
|
-
console.log(`
|
|
887
|
-
console.log(`
|
|
888
|
-
`
|
|
889
|
-
console.log(`${dim("Output:")}`);
|
|
890
|
-
console.log(` \u2022 Completeness percentage (0\u2013100) \u2014 how many schema fields are populated`);
|
|
891
|
-
console.log(` \u2022 Required field percentage \u2014 coverage of required fields only`);
|
|
892
|
-
console.log(` \u2022 Top missing field paths \u2014 the next fields to target
|
|
827
|
+
console.log(`${dim("Validation checks:")}`);
|
|
828
|
+
console.log(` \u2022 Manifest structure (org, orgSlug, sensors, calibration, timing, measurements)`);
|
|
829
|
+
console.log(` \u2022 Sensor ids unique and kebab-case`);
|
|
830
|
+
console.log(` \u2022 recordsMapping values exist in the records enums or carry proposed* flags
|
|
893
831
|
`);
|
|
894
832
|
console.log(`${dim("Examples:")}`);
|
|
895
|
-
console.log(` disclosureos
|
|
896
|
-
console.log(` disclosureos
|
|
897
|
-
console.log(` disclosureos completeness ./out/*.json --json
|
|
833
|
+
console.log(` disclosureos manifest validate ./manifests/eldaeon-sensors.json`);
|
|
834
|
+
console.log(` disclosureos manifest validate ./manifests/ --recursive --json
|
|
898
835
|
`);
|
|
899
836
|
}
|
|
900
837
|
|
|
901
|
-
// src/commands/
|
|
902
|
-
import { deriveFieldPaths } from "@disclosureos/scoring";
|
|
838
|
+
// src/commands/packet.ts
|
|
903
839
|
import {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
840
|
+
constants,
|
|
841
|
+
openSync,
|
|
842
|
+
closeSync,
|
|
843
|
+
fstatSync,
|
|
844
|
+
readSync,
|
|
845
|
+
lstatSync
|
|
846
|
+
} from "fs";
|
|
847
|
+
import { resolve as resolve2, dirname, join } from "path";
|
|
848
|
+
import { fileURLToPath } from "url";
|
|
849
|
+
import { createHash } from "crypto";
|
|
909
850
|
import {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
851
|
+
ReproductionPacketSchema,
|
|
852
|
+
evaluateReproductionPacket
|
|
853
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
854
|
+
import { parseExperimentalClaimHistory } from "@disclosureos/records/experimental/v2";
|
|
855
|
+
var MANIFEST_LIMIT = 8 * 1024 * 1024;
|
|
856
|
+
var TOTAL_LIMIT = 256 * 1024 * 1024;
|
|
857
|
+
var FILE_LIMIT = 4096;
|
|
858
|
+
var hash = (bytes2) => createHash("sha256").update(bytes2).digest("hex");
|
|
859
|
+
function readLocal(path, limit) {
|
|
860
|
+
const fd = openSync(
|
|
861
|
+
path,
|
|
862
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
863
|
+
);
|
|
864
|
+
try {
|
|
865
|
+
const stat2 = fstatSync(fd);
|
|
866
|
+
if (!stat2.isFile()) throw Error("Expected a regular file.");
|
|
867
|
+
if (stat2.size > limit) throw Error("Packet exceeds the CLI memory limit.");
|
|
868
|
+
const chunks = [];
|
|
869
|
+
let size = 0;
|
|
870
|
+
for (; ; ) {
|
|
871
|
+
const chunk = Buffer.alloc(Math.min(65536, limit - size + 1));
|
|
872
|
+
const count = readSync(fd, chunk, 0, chunk.length, null);
|
|
873
|
+
if (!count) break;
|
|
874
|
+
size += count;
|
|
875
|
+
if (size > limit) throw Error("Packet exceeds the CLI memory limit.");
|
|
876
|
+
chunks.push(chunk.subarray(0, count));
|
|
877
|
+
}
|
|
878
|
+
return Buffer.concat(chunks, size);
|
|
879
|
+
} finally {
|
|
880
|
+
closeSync(fd);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
var json = (bytes2) => JSON.parse(
|
|
884
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2)
|
|
885
|
+
);
|
|
886
|
+
var object = (value) => {
|
|
887
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
888
|
+
throw Error("Expected a JSON object.");
|
|
889
|
+
return value;
|
|
890
|
+
};
|
|
891
|
+
function usage() {
|
|
892
|
+
console.log(`DisclosureOS experimental packet workflow
|
|
893
|
+
|
|
894
|
+
disclosureos packet inspect <packet.json> [--json]
|
|
895
|
+
disclosureos packet validate <packet.json> [--json]
|
|
896
|
+
disclosureos packet reproduce <packet.json> [--json]
|
|
897
|
+
|
|
898
|
+
Files are read from files/<file ID> beside packet.json. Inspection reads only the
|
|
899
|
+
manifest; validation verifies files and research prerequisites. Reproduction runs
|
|
900
|
+
only the built-in synthetic-radial-velocity-mean v1 control, never bundled code.
|
|
901
|
+
Limits: 8 MiB manifest, 4096 files, 256 MiB combined file bytes. No network access.
|
|
902
|
+
Exit codes: 0 success, 1 failed or incomplete check, 2 usage or manifest-read error.
|
|
903
|
+
Use --json for the complete validator diagnostics and unchecked scientific limits.`);
|
|
904
|
+
}
|
|
905
|
+
function emit(output, jsonMode) {
|
|
906
|
+
if (jsonMode) {
|
|
907
|
+
console.log(JSON.stringify(output, null, 2));
|
|
920
908
|
return;
|
|
921
909
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
910
|
+
console.log(
|
|
911
|
+
`DisclosureOS packet ${output.command}: ${output.success ? "passed" : "failed or incomplete"}`
|
|
912
|
+
);
|
|
913
|
+
if (output.packetId)
|
|
914
|
+
console.log(
|
|
915
|
+
`Packet: ${output.packetId}
|
|
916
|
+
Manifest SHA-256: ${output.manifestSha256}`
|
|
917
|
+
);
|
|
918
|
+
if (output.inspection)
|
|
919
|
+
console.log(
|
|
920
|
+
`${output.inspection.fileCount} declared files; ${output.inspection.declaredBytes} bytes.
|
|
921
|
+
File identity and research prerequisites: not checked.
|
|
922
|
+
Next: disclosureos packet validate <packet.json>`
|
|
923
|
+
);
|
|
924
|
+
if (output.validation) {
|
|
925
|
+
const v = output.validation;
|
|
926
|
+
console.log(
|
|
927
|
+
`Structural: ${v.checks.structural}; semantic: ${v.checks.semantic}; profile: ${v.checks.profile}; external: ${v.checks.external}`
|
|
928
|
+
);
|
|
929
|
+
for (const i of v.issues)
|
|
930
|
+
console.log(`${i.code} ${i.pointer}: ${i.message}`);
|
|
931
|
+
if (v.success && output.command === "validate")
|
|
932
|
+
console.log(
|
|
933
|
+
"Next: disclosureos packet reproduce <packet.json> (synthetic control only)."
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
if (output.reproduction) {
|
|
937
|
+
const r = output.reproduction;
|
|
938
|
+
console.log(
|
|
939
|
+
`Reproduction: ${r.status}${r.value === void 0 ? "" : `; ${r.value} ${r.unit}, tolerance ${r.absoluteTolerance}`}`
|
|
940
|
+
);
|
|
941
|
+
if (r.message) console.log(r.message);
|
|
935
942
|
}
|
|
943
|
+
for (const i of output.issues)
|
|
944
|
+
console.log(`${i.code} ${i.pointer}: ${i.message}`);
|
|
945
|
+
console.log("Scientific eligibility: not checked.");
|
|
936
946
|
}
|
|
937
|
-
function
|
|
938
|
-
const
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
947
|
+
function reproduce(packet2, files) {
|
|
948
|
+
const method = packet2.method;
|
|
949
|
+
const result = {
|
|
950
|
+
status: "not_checked",
|
|
951
|
+
executor: "disclosureos-cli/synthetic-mean/0.1.0",
|
|
952
|
+
runtime: process.version,
|
|
953
|
+
methodId: method.id,
|
|
954
|
+
methodVersion: method.version,
|
|
955
|
+
unit: method.unit,
|
|
956
|
+
absoluteTolerance: method.absoluteTolerance
|
|
957
|
+
};
|
|
958
|
+
if (method.id !== "synthetic-radial-velocity-mean" || method.version !== "1") {
|
|
959
|
+
result.message = "Unsupported method. This CLI never executes packet-supplied implementations.";
|
|
960
|
+
return result;
|
|
946
961
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
962
|
+
try {
|
|
963
|
+
result.executorSha256 = hash(
|
|
964
|
+
readLocal(fileURLToPath(import.meta.url), TOTAL_LIMIT)
|
|
965
|
+
);
|
|
966
|
+
if (method.unit !== "m/s" || packet2.assets.context.find((a) => a.ref === "product:captured-raw")?.fileRef !== method.inputRef || packet2.assets.observation.find((a) => a.ref === "product:summary")?.fileRef !== method.expectedOutputRef)
|
|
967
|
+
throw Error("Method does not match the fixed synthetic control mapping.");
|
|
968
|
+
const input = object(json(files.get(method.inputRef))), expected = object(json(files.get(method.expectedOutputRef)));
|
|
969
|
+
const samples = input["samples"];
|
|
970
|
+
if (!Array.isArray(samples) || !samples.length || !samples.every(
|
|
971
|
+
(v) => typeof v === "number" && Number.isFinite(v)
|
|
972
|
+
))
|
|
973
|
+
throw Error("Expected a nonempty array of finite samples.");
|
|
974
|
+
let sum = 0;
|
|
975
|
+
for (const value2 of samples) sum += value2;
|
|
976
|
+
const value = sum / samples.length, expectedValue = expected["velocity"];
|
|
977
|
+
if (!Number.isFinite(value) || typeof expectedValue !== "number" || !Number.isFinite(expectedValue))
|
|
978
|
+
throw Error("Nonfinite or malformed calculation result.");
|
|
979
|
+
result.value = value;
|
|
980
|
+
result.expected = expectedValue;
|
|
981
|
+
const history2 = parseExperimentalClaimHistory(
|
|
982
|
+
json(files.get(packet2.documents.history))
|
|
983
|
+
);
|
|
984
|
+
const measurement = history2.success ? history2.data.observation.measurements.find(
|
|
985
|
+
(m) => `measurement:${m.id}` === method.measurementRef
|
|
986
|
+
) : void 0;
|
|
987
|
+
if (!measurement || measurement.value.unit !== method.unit || Math.abs(value - expectedValue) > method.absoluteTolerance || Math.abs(value - measurement.value.value) > method.absoluteTolerance)
|
|
988
|
+
throw Error(
|
|
989
|
+
"Computed result differs from the expected output or measurement beyond the declared tolerance."
|
|
990
|
+
);
|
|
991
|
+
result.status = "passed";
|
|
992
|
+
return result;
|
|
993
|
+
} catch (error2) {
|
|
994
|
+
result.status = "failed";
|
|
995
|
+
result.message = error2 instanceof Error ? error2.message : "Calculation failed.";
|
|
996
|
+
return result;
|
|
969
997
|
}
|
|
970
|
-
console.log(`
|
|
971
|
-
${dim(`Total: ${paths.length} schema-derived field paths`)}
|
|
972
|
-
`);
|
|
973
998
|
}
|
|
974
|
-
function
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (!biologicsOnly) {
|
|
979
|
-
console.log(`
|
|
980
|
-
Technology Observables (AATIP-derived)
|
|
981
|
-
`);
|
|
982
|
-
const techObs = Object.values(TECHNOLOGY_OBSERVABLES);
|
|
983
|
-
for (const obs of techObs) {
|
|
984
|
-
console.log(` ${obs.code} ${obs.label}`);
|
|
985
|
-
if (obs.description) {
|
|
986
|
-
console.log(` ${dim(obs.description)}`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
999
|
+
async function packet(args2) {
|
|
1000
|
+
if (args2.flags["help"]) {
|
|
1001
|
+
usage();
|
|
1002
|
+
return;
|
|
989
1003
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1004
|
+
const output = {
|
|
1005
|
+
command: args2.subcommand,
|
|
1006
|
+
success: false,
|
|
1007
|
+
issues: [],
|
|
1008
|
+
scientificEligibility: "not_checked"
|
|
1009
|
+
};
|
|
1010
|
+
const jsonMode = !!args2.flags["json"];
|
|
1011
|
+
if (!["inspect", "validate", "reproduce"].includes(args2.subcommand) || args2.positional.length !== 1 || Object.keys(args2.flags).some((f) => !["json", "help"].includes(f))) {
|
|
1012
|
+
output.issues.push({
|
|
1013
|
+
code: "CLI.USAGE",
|
|
1014
|
+
pointer: "",
|
|
1015
|
+
message: "Use packet inspect|validate|reproduce <packet.json> [--json]."
|
|
1016
|
+
});
|
|
1017
|
+
emit(output, jsonMode);
|
|
1018
|
+
if (!jsonMode) usage();
|
|
1019
|
+
process.exitCode = 2;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
let path, bytes2, input;
|
|
1023
|
+
try {
|
|
1024
|
+
path = resolve2(args2.positional[0]);
|
|
1025
|
+
bytes2 = readLocal(path, MANIFEST_LIMIT);
|
|
1026
|
+
input = json(bytes2);
|
|
1027
|
+
output.manifestSha256 = hash(bytes2);
|
|
1028
|
+
} catch (error2) {
|
|
1029
|
+
output.issues.push({
|
|
1030
|
+
code: "CLI.MANIFEST",
|
|
1031
|
+
pointer: "",
|
|
1032
|
+
message: error2 instanceof Error ? error2.message : "Cannot read manifest."
|
|
1033
|
+
});
|
|
1034
|
+
emit(output, jsonMode);
|
|
1035
|
+
process.exitCode = 2;
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const parsed = ReproductionPacketSchema.safeParse(input);
|
|
1039
|
+
if (!parsed.success) {
|
|
1040
|
+
output.validation = await evaluateReproductionPacket(input);
|
|
1041
|
+
emit(output, jsonMode);
|
|
1042
|
+
process.exitCode = 1;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const data = parsed.data;
|
|
1046
|
+
output.packetId = data.id;
|
|
1047
|
+
if (args2.subcommand === "inspect") {
|
|
1048
|
+
output.inspection = {
|
|
1049
|
+
fileCount: data.files.length,
|
|
1050
|
+
declaredBytes: data.files.reduce((n, f) => n + f.byteLength, 0),
|
|
1051
|
+
documents: data.documents,
|
|
1052
|
+
method: data.method,
|
|
1053
|
+
fileIdentity: "not_checked",
|
|
1054
|
+
researchPrerequisites: "not_checked"
|
|
1055
|
+
};
|
|
1056
|
+
output.success = true;
|
|
1057
|
+
emit(output, jsonMode);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const files = /* @__PURE__ */ new Map();
|
|
1061
|
+
if (data.files.length > FILE_LIMIT || data.files.reduce((n, f) => n + f.byteLength, 0) > TOTAL_LIMIT) {
|
|
1062
|
+
output.issues.push({
|
|
1063
|
+
code: "CLI.LIMIT",
|
|
1064
|
+
pointer: "/files",
|
|
1065
|
+
message: "Declared packet exceeds 4096 files or 256 MiB."
|
|
1066
|
+
});
|
|
1067
|
+
emit(output, jsonMode);
|
|
1068
|
+
process.exitCode = 1;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const directory5 = join(dirname(path), "files");
|
|
1072
|
+
let remaining = TOTAL_LIMIT;
|
|
1073
|
+
try {
|
|
1074
|
+
if (!lstatSync(directory5).isDirectory())
|
|
1075
|
+
throw Error("files must be a real directory, not a symlink.");
|
|
1076
|
+
} catch (error2) {
|
|
1077
|
+
output.issues.push({
|
|
1078
|
+
code: "CLI.FILES_DIRECTORY",
|
|
1079
|
+
pointer: "/files",
|
|
1080
|
+
message: error2 instanceof Error ? error2.message : "Cannot read files directory."
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
if (!output.issues.length)
|
|
1084
|
+
for (const [i, file] of data.files.entries())
|
|
1085
|
+
try {
|
|
1086
|
+
if (files.has(file.id)) continue;
|
|
1087
|
+
const bytes3 = readLocal(join(directory5, file.id), remaining);
|
|
1088
|
+
files.set(file.id, bytes3);
|
|
1089
|
+
remaining -= bytes3.byteLength;
|
|
1090
|
+
} catch (error2) {
|
|
1091
|
+
output.issues.push({
|
|
1092
|
+
code: "CLI.FILE_READ",
|
|
1093
|
+
pointer: `/files/${i}`,
|
|
1094
|
+
message: error2 instanceof Error ? error2.message : "Cannot read file."
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
output.validation = await evaluateReproductionPacket(input, { files });
|
|
1098
|
+
output.success = output.validation.success && !output.issues.length;
|
|
1099
|
+
if (args2.subcommand === "reproduce" && output.success) {
|
|
1100
|
+
output.reproduction = reproduce(data, files);
|
|
1101
|
+
output.success = output.reproduction.status === "passed";
|
|
1102
|
+
}
|
|
1103
|
+
emit(output, jsonMode);
|
|
1104
|
+
if (!output.success) process.exitCode = 1;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/commands/dataset.ts
|
|
1108
|
+
import { lstatSync as lstatSync2 } from "fs";
|
|
1109
|
+
import { resolve as resolve3, dirname as dirname2, join as join2 } from "path";
|
|
1110
|
+
import { createHash as createHash2 } from "crypto";
|
|
1111
|
+
import {
|
|
1112
|
+
DatasetReleaseSchema,
|
|
1113
|
+
ReproductionPacketSchema as ReproductionPacketSchema2,
|
|
1114
|
+
evaluateDatasetRelease
|
|
1115
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
1116
|
+
var LIMIT = 256 * 1024 * 1024;
|
|
1117
|
+
var MANIFEST_LIMIT2 = 8 * 1024 * 1024;
|
|
1118
|
+
var json2 = (bytes2) => JSON.parse(
|
|
1119
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2)
|
|
1120
|
+
);
|
|
1121
|
+
function directory(path) {
|
|
1122
|
+
if (!lstatSync2(path).isDirectory())
|
|
1123
|
+
throw Error("Expected a real directory, not a symlink.");
|
|
1124
|
+
}
|
|
1125
|
+
async function dataset(args2) {
|
|
1126
|
+
if (args2.flags["help"]) {
|
|
1127
|
+
console.log(
|
|
1128
|
+
"DisclosureOS experimental dataset workflow\n\n disclosureos dataset inspect <dataset.json> [--json]\n disclosureos dataset validate <dataset.json> [--json]\n\nMember packets live at packets/<entry ID>/packet.json and files/<file ID>.\nInspection reads only the release manifest. Validation checks pinned packets and\nnominal session membership. No code execution or inferred scientific eligibility.\nLimits: 128 packets, 4096 total files, 256 MiB total packet/file bytes.\nExit: 0 success, 1 failed/incomplete, 2 usage or release-manifest read error."
|
|
1129
|
+
);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const output = {
|
|
1133
|
+
command: args2.subcommand,
|
|
1134
|
+
success: false,
|
|
1135
|
+
issues: [],
|
|
1136
|
+
scientificEligibility: "not_checked",
|
|
1137
|
+
authorization: "not_checked"
|
|
1138
|
+
};
|
|
1139
|
+
const emit3 = () => {
|
|
1140
|
+
if (args2.flags["json"]) {
|
|
1141
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
console.log(
|
|
1145
|
+
`DisclosureOS dataset ${args2.subcommand}: ${output.success ? "passed" : "failed or incomplete"}`
|
|
1146
|
+
);
|
|
1147
|
+
if (output.manifestSha256)
|
|
1148
|
+
console.log(`Manifest SHA-256: ${output.manifestSha256}`);
|
|
1149
|
+
if (output.inspection)
|
|
1150
|
+
console.log(JSON.stringify(output.inspection, null, 2));
|
|
1151
|
+
if (output.validation) {
|
|
1152
|
+
console.log(JSON.stringify(output.validation.checks));
|
|
1153
|
+
for (const i of output.validation.issues)
|
|
1154
|
+
console.log(`${i.code} ${i.pointer}: ${i.message}`);
|
|
1155
|
+
for (const packet2 of output.validation.packets)
|
|
1156
|
+
for (const i of packet2.validation?.issues ?? [])
|
|
1157
|
+
console.log(
|
|
1158
|
+
`${packet2.packetRef} ${i.code} ${i.pointer}: ${i.message}`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
for (const i of output.issues)
|
|
1162
|
+
console.log(`${i.code} ${i.pointer}: ${i.message}`);
|
|
1163
|
+
console.log("Scientific eligibility and authorization: not checked.");
|
|
1164
|
+
};
|
|
1165
|
+
if (!["inspect", "validate"].includes(args2.subcommand) || args2.positional.length !== 1 || Object.keys(args2.flags).some((f) => !["json", "help"].includes(f))) {
|
|
1166
|
+
output.issues.push({
|
|
1167
|
+
code: "CLI.USAGE",
|
|
1168
|
+
pointer: "",
|
|
1169
|
+
message: "Use dataset inspect|validate <dataset.json> [--json]."
|
|
1170
|
+
});
|
|
1171
|
+
emit3();
|
|
1172
|
+
process.exitCode = 2;
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
let input, path;
|
|
1176
|
+
try {
|
|
1177
|
+
path = resolve3(args2.positional[0]);
|
|
1178
|
+
const bytes2 = readLocal(path, MANIFEST_LIMIT2);
|
|
1179
|
+
output.manifestSha256 = createHash2("sha256").update(bytes2).digest("hex");
|
|
1180
|
+
input = json2(bytes2);
|
|
1181
|
+
} catch (error2) {
|
|
1182
|
+
output.issues.push({
|
|
1183
|
+
code: "CLI.MANIFEST",
|
|
1184
|
+
pointer: "",
|
|
1185
|
+
message: error2 instanceof Error ? error2.message : "Cannot read dataset manifest."
|
|
1186
|
+
});
|
|
1187
|
+
emit3();
|
|
1188
|
+
process.exitCode = 2;
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const parsed = DatasetReleaseSchema.safeParse(input);
|
|
1192
|
+
if (!parsed.success) {
|
|
1193
|
+
output.validation = await evaluateDatasetRelease(input);
|
|
1194
|
+
emit3();
|
|
1195
|
+
process.exitCode = 1;
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const release = parsed.data;
|
|
1199
|
+
if (args2.subcommand === "inspect") {
|
|
1200
|
+
output.inspection = {
|
|
1201
|
+
id: release.id,
|
|
1202
|
+
version: release.version,
|
|
1203
|
+
packetCount: release.packets.length,
|
|
1204
|
+
sessions: release.sessions,
|
|
1205
|
+
access: release.access,
|
|
1206
|
+
license: release.license,
|
|
1207
|
+
fileIdentity: "not_checked",
|
|
1208
|
+
membership: "not_checked"
|
|
1209
|
+
};
|
|
1210
|
+
output.success = true;
|
|
1211
|
+
emit3();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (release.packets.length > 128) {
|
|
1215
|
+
output.issues.push({
|
|
1216
|
+
code: "CLI.LIMIT",
|
|
1217
|
+
pointer: "/packets",
|
|
1218
|
+
message: "At most 128 packets are supported."
|
|
1219
|
+
});
|
|
1220
|
+
emit3();
|
|
1221
|
+
process.exitCode = 1;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const packets = /* @__PURE__ */ new Map();
|
|
1225
|
+
let remaining = LIMIT, count = 0;
|
|
1226
|
+
const root = join2(dirname2(path), "packets");
|
|
1227
|
+
try {
|
|
1228
|
+
directory(root);
|
|
1229
|
+
} catch (error2) {
|
|
1230
|
+
output.issues.push({
|
|
1231
|
+
code: "CLI.PACKET_DIRECTORY",
|
|
1232
|
+
pointer: "/packets",
|
|
1233
|
+
message: error2 instanceof Error ? error2.message : "Cannot read packets directory."
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
if (!output.issues.length)
|
|
1237
|
+
for (const [i, entry] of release.packets.entries()) {
|
|
1238
|
+
if (packets.has(entry.id)) continue;
|
|
1239
|
+
const base = join2(root, entry.id), issuesPath = `/packets/${i}`;
|
|
1240
|
+
try {
|
|
1241
|
+
directory(base);
|
|
1242
|
+
const manifest2 = readLocal(
|
|
1243
|
+
join2(base, "packet.json"),
|
|
1244
|
+
Math.min(remaining, MANIFEST_LIMIT2)
|
|
1245
|
+
);
|
|
1246
|
+
remaining -= manifest2.byteLength;
|
|
1247
|
+
const files = /* @__PURE__ */ new Map();
|
|
1248
|
+
packets.set(entry.id, { manifest: manifest2, files });
|
|
1249
|
+
const packet2 = ReproductionPacketSchema2.safeParse(json2(manifest2));
|
|
1250
|
+
if (!packet2.success) continue;
|
|
1251
|
+
if (count + packet2.data.files.length > 4096)
|
|
1252
|
+
throw Error("Dataset exceeds the 4096-file limit.");
|
|
1253
|
+
count += packet2.data.files.length;
|
|
1254
|
+
directory(join2(base, "files"));
|
|
1255
|
+
for (const file of packet2.data.files) {
|
|
1256
|
+
if (files.has(file.id)) continue;
|
|
1257
|
+
try {
|
|
1258
|
+
const bytes2 = readLocal(join2(base, "files", file.id), remaining);
|
|
1259
|
+
remaining -= bytes2.byteLength;
|
|
1260
|
+
files.set(file.id, bytes2);
|
|
1261
|
+
} catch (error2) {
|
|
1262
|
+
output.issues.push({
|
|
1263
|
+
code: "CLI.FILE_READ",
|
|
1264
|
+
pointer: issuesPath,
|
|
1265
|
+
message: error2 instanceof Error ? error2.message : "Cannot read member file."
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
} catch (error2) {
|
|
1270
|
+
output.issues.push({
|
|
1271
|
+
code: "CLI.PACKET_READ",
|
|
1272
|
+
pointer: issuesPath,
|
|
1273
|
+
message: error2 instanceof Error ? error2.message : "Cannot read member packet."
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
output.validation = await evaluateDatasetRelease(input, { packets });
|
|
1278
|
+
output.success = output.validation.success && !output.issues.length;
|
|
1279
|
+
emit3();
|
|
1280
|
+
if (!output.success) process.exitCode = 1;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/commands/intake.ts
|
|
1284
|
+
import { mkdirSync, writeFileSync as writeFileSync2, rmSync, lstatSync as lstatSync3 } from "fs";
|
|
1285
|
+
import { resolve as resolve4, dirname as dirname3, join as join3, basename } from "path";
|
|
1286
|
+
import { randomUUID, createHash as createHash3 } from "crypto";
|
|
1287
|
+
import {
|
|
1288
|
+
SourceIntakeSchema,
|
|
1289
|
+
evaluateSourceIntake
|
|
1290
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
1291
|
+
var LIMIT2 = 256 * 1024 * 1024;
|
|
1292
|
+
var hash2 = (bytes2) => createHash3("sha256").update(bytes2).digest("hex");
|
|
1293
|
+
var decode = (bytes2) => JSON.parse(
|
|
1294
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2)
|
|
1295
|
+
);
|
|
1296
|
+
var unknown = () => ({
|
|
1297
|
+
state: "unknown",
|
|
1298
|
+
reason: "Not established at file intake."
|
|
1299
|
+
});
|
|
1300
|
+
async function intake(args2) {
|
|
1301
|
+
if (args2.flags["help"]) {
|
|
1302
|
+
console.log(
|
|
1303
|
+
"DisclosureOS experimental source intake\n\n disclosureos intake create <file...> --out <new-directory> [--json]\n disclosureos intake inspect <intake.json> [--json]\n disclosureos intake validate <intake.json> [--json]\n\nCreate copies exact bytes into files/<ID> and records receipt time and hashes.\nContext, format, role, access and licensing remain explicitly unknown. No decoding\nor assessment is inferred. Destination must not exist. Limits: 4096 files,\n256 MiB combined bytes, 8 MiB manifest. No network access.\nExit: 0 success, 1 failed/incomplete, 2 usage or file-operation error."
|
|
1304
|
+
);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const output = {
|
|
1308
|
+
command: args2.subcommand,
|
|
1309
|
+
success: false,
|
|
1310
|
+
issues: [],
|
|
1311
|
+
researchEligibility: "not_checked"
|
|
1312
|
+
};
|
|
1313
|
+
const emit3 = () => {
|
|
1314
|
+
if (args2.flags["json"]) {
|
|
1315
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
console.log(
|
|
1319
|
+
`DisclosureOS intake ${args2.subcommand}: ${output.success ? "passed" : "failed or incomplete"}`
|
|
1320
|
+
);
|
|
1321
|
+
if (output.path) console.log(`Receipt: ${output.path}`);
|
|
1322
|
+
if (output.manifestSha256)
|
|
1323
|
+
console.log(`Manifest SHA-256: ${output.manifestSha256}`);
|
|
1324
|
+
if (output.inspection) {
|
|
1325
|
+
console.log(
|
|
1326
|
+
`${output.inspection.artifacts.length} artifacts; file identity not checked.`
|
|
1327
|
+
);
|
|
1328
|
+
console.log(JSON.stringify(output.inspection, null, 2));
|
|
1329
|
+
}
|
|
1330
|
+
if (output.validation) {
|
|
1331
|
+
console.log(JSON.stringify(output.validation.checks));
|
|
1332
|
+
for (const i of output.validation.issues)
|
|
1333
|
+
console.log(`${i.code} ${i.pointer}: ${i.message}`);
|
|
1334
|
+
for (const gap of output.validation.gaps)
|
|
1335
|
+
console.log(
|
|
1336
|
+
`MISSING ${gap.pointer}: ${gap.reason} Next: ${gap.nextAction}`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
for (const i of output.issues)
|
|
1340
|
+
console.log(`${i.code} ${i.pointer}: ${i.message}`);
|
|
1341
|
+
console.log(
|
|
1342
|
+
"Research eligibility, artifact contents and authorization: not checked."
|
|
1343
|
+
);
|
|
1344
|
+
};
|
|
1345
|
+
const create = args2.subcommand === "create";
|
|
1346
|
+
if (!["create", "inspect", "validate"].includes(args2.subcommand) || Object.keys(args2.flags).some(
|
|
1347
|
+
(f) => !["help", "json", ...create ? ["out"] : []].includes(f)
|
|
1348
|
+
) || (create ? args2.positional.length < 1 || typeof args2.flags["out"] !== "string" : args2.positional.length !== 1)) {
|
|
1349
|
+
output.issues.push({
|
|
1350
|
+
code: "CLI.USAGE",
|
|
1351
|
+
pointer: "",
|
|
1352
|
+
message: "Use intake create <file...> --out <new-directory>, or inspect|validate <intake.json>."
|
|
1353
|
+
});
|
|
1354
|
+
emit3();
|
|
1355
|
+
process.exitCode = 2;
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const files = /* @__PURE__ */ new Map();
|
|
1359
|
+
let remaining = LIMIT2;
|
|
1360
|
+
if (create) {
|
|
1361
|
+
let destination, created = false;
|
|
1362
|
+
try {
|
|
1363
|
+
if (args2.positional.length > 4096)
|
|
1364
|
+
throw Error("At most 4096 source files are supported.");
|
|
1365
|
+
const artifacts = [];
|
|
1366
|
+
for (const [i, path2] of args2.positional.entries()) {
|
|
1367
|
+
const bytes3 = readLocal(resolve4(path2), remaining);
|
|
1368
|
+
remaining -= bytes3.length;
|
|
1369
|
+
const id = `file-${i + 1}`;
|
|
1370
|
+
files.set(id, bytes3);
|
|
1371
|
+
artifacts.push({
|
|
1372
|
+
id,
|
|
1373
|
+
originalName: basename(path2),
|
|
1374
|
+
role: "unknown",
|
|
1375
|
+
format: unknown(),
|
|
1376
|
+
sha256: hash2(bytes3),
|
|
1377
|
+
byteLength: bytes3.length,
|
|
1378
|
+
context: {
|
|
1379
|
+
instrument: unknown(),
|
|
1380
|
+
calibration: unknown(),
|
|
1381
|
+
clock: unknown(),
|
|
1382
|
+
acquisitionTime: unknown()
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
const receipt = {
|
|
1387
|
+
kind: "source_intake",
|
|
1388
|
+
schemaVersion: "0.1.0",
|
|
1389
|
+
id: `intake-${randomUUID()}`,
|
|
1390
|
+
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1391
|
+
source: { publisher: unknown() },
|
|
1392
|
+
access: "unknown",
|
|
1393
|
+
license: unknown(),
|
|
1394
|
+
artifacts
|
|
1395
|
+
};
|
|
1396
|
+
const bytes2 = Buffer.from(JSON.stringify(receipt, null, 2) + "\n");
|
|
1397
|
+
if (bytes2.length > 8 * 1024 * 1024)
|
|
1398
|
+
throw Error("Generated manifest exceeds 8 MiB.");
|
|
1399
|
+
output.validation = await evaluateSourceIntake(receipt, { files });
|
|
1400
|
+
if (!output.validation.success)
|
|
1401
|
+
throw Error("Receipt did not pass file identity checks.");
|
|
1402
|
+
destination = resolve4(args2.flags["out"]);
|
|
1403
|
+
mkdirSync(destination, { mode: 448 });
|
|
1404
|
+
created = true;
|
|
1405
|
+
mkdirSync(join3(destination, "files"), { mode: 448 });
|
|
1406
|
+
for (const [id, b] of files)
|
|
1407
|
+
writeFileSync2(join3(destination, "files", id), b, {
|
|
1408
|
+
flag: "wx",
|
|
1409
|
+
mode: 384
|
|
1410
|
+
});
|
|
1411
|
+
writeFileSync2(join3(destination, "intake.json"), bytes2, {
|
|
1412
|
+
flag: "wx",
|
|
1413
|
+
mode: 384
|
|
1414
|
+
});
|
|
1415
|
+
output.path = join3(destination, "intake.json");
|
|
1416
|
+
output.manifestSha256 = hash2(bytes2);
|
|
1417
|
+
output.success = true;
|
|
1418
|
+
} catch (error2) {
|
|
1419
|
+
if (created && destination)
|
|
1420
|
+
rmSync(destination, { recursive: true, force: true });
|
|
1421
|
+
output.issues.push({
|
|
1422
|
+
code: "CLI.INTAKE_CREATE",
|
|
1423
|
+
pointer: "",
|
|
1424
|
+
message: error2 instanceof Error ? error2.message : "Intake creation failed."
|
|
1425
|
+
});
|
|
1426
|
+
process.exitCode = 2;
|
|
1427
|
+
}
|
|
1428
|
+
emit3();
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
let input, path;
|
|
1432
|
+
try {
|
|
1433
|
+
path = resolve4(args2.positional[0]);
|
|
1434
|
+
const bytes2 = readLocal(path, 8 * 1024 * 1024);
|
|
1435
|
+
output.manifestSha256 = hash2(bytes2);
|
|
1436
|
+
input = decode(bytes2);
|
|
1437
|
+
} catch (error2) {
|
|
1438
|
+
output.issues.push({
|
|
1439
|
+
code: "CLI.MANIFEST",
|
|
1440
|
+
pointer: "",
|
|
1441
|
+
message: error2 instanceof Error ? error2.message : "Cannot read intake manifest."
|
|
1442
|
+
});
|
|
1443
|
+
emit3();
|
|
1444
|
+
process.exitCode = 2;
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const parsed = SourceIntakeSchema.safeParse(input);
|
|
1448
|
+
if (!parsed.success) {
|
|
1449
|
+
output.validation = await evaluateSourceIntake(input);
|
|
1450
|
+
emit3();
|
|
1451
|
+
process.exitCode = 1;
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (args2.subcommand === "inspect") {
|
|
1455
|
+
output.inspection = parsed.data;
|
|
1456
|
+
output.fileIdentity = "not_checked";
|
|
1457
|
+
output.success = true;
|
|
1458
|
+
emit3();
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (parsed.data.artifacts.length > 4096 || parsed.data.artifacts.reduce((n, a) => n + a.byteLength, 0) > LIMIT2) {
|
|
1462
|
+
output.issues.push({
|
|
1463
|
+
code: "CLI.LIMIT",
|
|
1464
|
+
pointer: "/artifacts",
|
|
1465
|
+
message: "Intake exceeds 4096 artifacts or 256 MiB."
|
|
1466
|
+
});
|
|
1467
|
+
emit3();
|
|
1468
|
+
process.exitCode = 1;
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const directory5 = join3(dirname3(path), "files");
|
|
1472
|
+
try {
|
|
1473
|
+
if (!lstatSync3(directory5).isDirectory())
|
|
1474
|
+
throw Error("Expected a real files directory, not a symlink.");
|
|
1475
|
+
} catch (error2) {
|
|
1476
|
+
output.issues.push({
|
|
1477
|
+
code: "CLI.FILES_DIRECTORY",
|
|
1478
|
+
pointer: "/artifacts",
|
|
1479
|
+
message: error2 instanceof Error ? error2.message : "Cannot read files directory."
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
if (!output.issues.length)
|
|
1483
|
+
for (const [i, a] of parsed.data.artifacts.entries())
|
|
1484
|
+
try {
|
|
1485
|
+
if (files.has(a.id)) continue;
|
|
1486
|
+
const b = readLocal(join3(directory5, a.id), remaining);
|
|
1487
|
+
remaining -= b.length;
|
|
1488
|
+
files.set(a.id, b);
|
|
1489
|
+
} catch (error2) {
|
|
1490
|
+
output.issues.push({
|
|
1491
|
+
code: "CLI.FILE_READ",
|
|
1492
|
+
pointer: `/artifacts/${i}`,
|
|
1493
|
+
message: error2 instanceof Error ? error2.message : "Cannot read artifact."
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
output.validation = await evaluateSourceIntake(input, { files });
|
|
1497
|
+
output.success = output.validation.success && !output.issues.length;
|
|
1498
|
+
emit3();
|
|
1499
|
+
if (!output.success) process.exitCode = 1;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// src/commands/profile-prepare.ts
|
|
1503
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync as rmSync2, lstatSync as lstatSync4 } from "fs";
|
|
1504
|
+
import { resolve as resolve5, dirname as dirname4, join as join4 } from "path";
|
|
1505
|
+
import { createHash as createHash4 } from "crypto";
|
|
1506
|
+
import {
|
|
1507
|
+
ProfilePreparationSchema,
|
|
1508
|
+
evaluateProfileEvaluation
|
|
1509
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
1510
|
+
var META = 8 * 1024 * 1024;
|
|
1511
|
+
var TOTAL = 256 * 1024 * 1024;
|
|
1512
|
+
var FILES = 4096;
|
|
1513
|
+
var pin = (bytes2) => ({
|
|
1514
|
+
sha256: createHash4("sha256").update(bytes2).digest("hex"),
|
|
1515
|
+
byteLength: bytes2.length
|
|
1516
|
+
});
|
|
1517
|
+
function directory2(path) {
|
|
1518
|
+
if (!lstatSync4(path).isDirectory())
|
|
1519
|
+
throw Error("Expected a real directory, not a symlink.");
|
|
1520
|
+
}
|
|
1521
|
+
var decode2 = (bytes2) => JSON.parse(
|
|
1522
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2)
|
|
1523
|
+
);
|
|
1524
|
+
async function prepareProfile(args2) {
|
|
1525
|
+
const output = {
|
|
1526
|
+
command: "prepare",
|
|
1527
|
+
success: false,
|
|
1528
|
+
issues: [],
|
|
1529
|
+
scientificEligibility: "not_checked"
|
|
1530
|
+
};
|
|
1531
|
+
let destination, created = false;
|
|
1532
|
+
const emit3 = () => {
|
|
1533
|
+
if (args2.flags["json"]) console.log(JSON.stringify(output, null, 2));
|
|
1534
|
+
else {
|
|
1535
|
+
console.log(
|
|
1536
|
+
"DisclosureOS profile preparation: " + (output.success ? "bundle prepared" : "not prepared")
|
|
1537
|
+
);
|
|
1538
|
+
if (output.path) console.log("Bundle: " + JSON.stringify(output.path));
|
|
1539
|
+
if (output.profileStatus)
|
|
1540
|
+
console.log(
|
|
1541
|
+
"Profile outcome: " + output.profileStatus + "; receipt " + (output.receiptCreated ? "created (unsigned)" : "not created")
|
|
1542
|
+
);
|
|
1543
|
+
for (const i of output.issues)
|
|
1544
|
+
console.log(i.code + ": " + JSON.stringify(i.message));
|
|
1545
|
+
console.log(
|
|
1546
|
+
"Preparation success is not a passing profile. Run profile check on evaluation.json for requirements and next actions."
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
if (args2.positional.length !== 1 || typeof args2.flags["out"] !== "string" || Object.keys(args2.flags).some((f) => !["json", "out", "help"].includes(f))) {
|
|
1551
|
+
output.issues.push({
|
|
1552
|
+
code: "CLI.USAGE",
|
|
1553
|
+
message: "Use profile prepare <plan.json> --out <new-directory> [--json]."
|
|
1554
|
+
});
|
|
1555
|
+
emit3();
|
|
1556
|
+
process.exitCode = 2;
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
try {
|
|
1560
|
+
const planPath = resolve5(args2.positional[0]), base = dirname4(planPath);
|
|
1561
|
+
directory2(base);
|
|
1562
|
+
const planBytes = readLocal(planPath, META), parsed = ProfilePreparationSchema.safeParse(decode2(planBytes));
|
|
1563
|
+
output.planSha256 = pin(planBytes).sha256;
|
|
1564
|
+
if (!parsed.success) {
|
|
1565
|
+
output.issues.push(
|
|
1566
|
+
...parsed.error.issues.map((i) => ({
|
|
1567
|
+
code: "PREPARE.PLAN",
|
|
1568
|
+
message: i.path.join("/") + ": " + i.message
|
|
1569
|
+
}))
|
|
1570
|
+
);
|
|
1571
|
+
emit3();
|
|
1572
|
+
process.exitCode = 1;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const plan = parsed.data, deps = [
|
|
1576
|
+
...plan.dependencies.vocabularies,
|
|
1577
|
+
plan.dependencies.implementation,
|
|
1578
|
+
plan.dependencies.environment
|
|
1579
|
+
];
|
|
1580
|
+
if (plan.assets.length + deps.length + 2 > FILES)
|
|
1581
|
+
throw Error("Preparation exceeds 4096 input files.");
|
|
1582
|
+
if (new Set(deps.map((d) => d.ref)).size !== deps.length || new Set(plan.dependencies.vocabularies.map((d) => d.id)).size !== plan.dependencies.vocabularies.length || new Set(plan.assets.map((a) => a.ref)).size !== plan.assets.length || new Set(plan.assets.map((a) => a.fileRef)).size !== plan.assets.length)
|
|
1583
|
+
throw Error("Duplicate dependency, vocabulary or asset references.");
|
|
1584
|
+
let remaining = TOTAL - planBytes.length;
|
|
1585
|
+
const read = (path, limit) => {
|
|
1586
|
+
const parts = path.split("/");
|
|
1587
|
+
let parent = base;
|
|
1588
|
+
for (const part of parts.slice(0, -1)) {
|
|
1589
|
+
parent = join4(parent, part);
|
|
1590
|
+
directory2(parent);
|
|
1591
|
+
}
|
|
1592
|
+
const bytes2 = readLocal(join4(base, ...parts), Math.min(limit, remaining));
|
|
1593
|
+
remaining -= bytes2.length;
|
|
1594
|
+
if (!bytes2.length)
|
|
1595
|
+
throw Error(
|
|
1596
|
+
"Empty inputs cannot receive positive-length evaluation pins: " + path
|
|
1597
|
+
);
|
|
1598
|
+
return bytes2;
|
|
1599
|
+
};
|
|
1600
|
+
const historyBytes = read(plan.history, META), selectionBytes = read(plan.selection, META);
|
|
1601
|
+
decode2(historyBytes);
|
|
1602
|
+
decode2(selectionBytes);
|
|
1603
|
+
const assetFiles = new Map(
|
|
1604
|
+
plan.assets.map((a) => [a.fileRef, read(a.path, remaining)])
|
|
1605
|
+
);
|
|
1606
|
+
const dependencyFiles = new Map(
|
|
1607
|
+
deps.map((d) => [d.ref, read(d.path, remaining)])
|
|
1608
|
+
);
|
|
1609
|
+
const dependency = (d) => ({
|
|
1610
|
+
ref: d.ref,
|
|
1611
|
+
id: d.id,
|
|
1612
|
+
version: d.version,
|
|
1613
|
+
...pin(dependencyFiles.get(d.ref))
|
|
1614
|
+
});
|
|
1615
|
+
const manifest2 = {
|
|
1616
|
+
kind: "profile_evaluation",
|
|
1617
|
+
schemaVersion: "0.1.0",
|
|
1618
|
+
id: plan.id,
|
|
1619
|
+
workflow: plan.workflow,
|
|
1620
|
+
history: pin(historyBytes),
|
|
1621
|
+
selection: pin(selectionBytes),
|
|
1622
|
+
assets: plan.assets.map((a) => ({
|
|
1623
|
+
ref: a.ref,
|
|
1624
|
+
fileRef: a.fileRef,
|
|
1625
|
+
...pin(assetFiles.get(a.fileRef))
|
|
1626
|
+
})),
|
|
1627
|
+
dependencies: {
|
|
1628
|
+
vocabularies: plan.dependencies.vocabularies.map(dependency),
|
|
1629
|
+
implementation: dependency(plan.dependencies.implementation),
|
|
1630
|
+
environment: dependency(plan.dependencies.environment)
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
const evaluation = await evaluateProfileEvaluation(
|
|
1634
|
+
manifest2,
|
|
1635
|
+
{ historyBytes, selectionBytes, assetFiles, dependencyFiles }
|
|
1636
|
+
);
|
|
1637
|
+
if (!evaluation.receipt)
|
|
1638
|
+
throw Error(
|
|
1639
|
+
"Pinned inputs did not produce a receipt: " + JSON.stringify(evaluation.issues)
|
|
1640
|
+
);
|
|
1641
|
+
const manifestBytes = Buffer.from(JSON.stringify(manifest2, null, 2) + "\n"), receiptBytes = Buffer.from(
|
|
1642
|
+
JSON.stringify(evaluation.receipt, null, 2) + "\n"
|
|
1643
|
+
);
|
|
1644
|
+
if (manifestBytes.length > META || receiptBytes.length > META)
|
|
1645
|
+
throw Error("Generated metadata exceeds 8 MiB.");
|
|
1646
|
+
const inputBytes = historyBytes.length + selectionBytes.length + [...assetFiles.values(), ...dependencyFiles.values()].reduce(
|
|
1647
|
+
(n, b) => n + b.length,
|
|
1648
|
+
0
|
|
1649
|
+
);
|
|
1650
|
+
if (inputBytes + manifestBytes.length + receiptBytes.length > TOTAL)
|
|
1651
|
+
throw Error("Generated bundle exceeds 256 MiB.");
|
|
1652
|
+
destination = resolve5(args2.flags["out"]);
|
|
1653
|
+
directory2(dirname4(destination));
|
|
1654
|
+
mkdirSync2(destination, { mode: 448 });
|
|
1655
|
+
created = true;
|
|
1656
|
+
for (const name2 of ["assets", "dependencies"])
|
|
1657
|
+
mkdirSync2(join4(destination, name2), { mode: 448 });
|
|
1658
|
+
const write = (path, bytes2) => writeFileSync3(join4(destination, path), bytes2, {
|
|
1659
|
+
flag: "wx",
|
|
1660
|
+
mode: 384
|
|
1661
|
+
});
|
|
1662
|
+
write("history.json", historyBytes);
|
|
1663
|
+
write("selection.json", selectionBytes);
|
|
1664
|
+
write("evaluation.json", manifestBytes);
|
|
1665
|
+
write("receipt.json", receiptBytes);
|
|
1666
|
+
for (const [ref, bytes2] of assetFiles) write("assets/" + ref, bytes2);
|
|
1667
|
+
for (const [ref, bytes2] of dependencyFiles)
|
|
1668
|
+
write("dependencies/" + ref, bytes2);
|
|
1669
|
+
output.success = true;
|
|
1670
|
+
output.path = destination;
|
|
1671
|
+
output.profileStatus = evaluation.checks.profile;
|
|
1672
|
+
output.profileSuccess = evaluation.success;
|
|
1673
|
+
output.receiptCreated = true;
|
|
1674
|
+
} catch (error2) {
|
|
1675
|
+
if (created && destination)
|
|
1676
|
+
rmSync2(destination, { recursive: true, force: true });
|
|
1677
|
+
output.issues.push({
|
|
1678
|
+
code: "PREPARE.INPUT_OR_WRITE",
|
|
1679
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
1680
|
+
});
|
|
1681
|
+
process.exitCode = 2;
|
|
1682
|
+
}
|
|
1683
|
+
emit3();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/commands/profile.ts
|
|
1687
|
+
import { lstatSync as lstatSync5 } from "fs";
|
|
1688
|
+
import { resolve as resolve6, dirname as dirname5, join as join5 } from "path";
|
|
1689
|
+
import { createHash as createHash5 } from "crypto";
|
|
1690
|
+
import {
|
|
1691
|
+
ProfileEvaluationSchema,
|
|
1692
|
+
evaluateProfileEvaluation as evaluateProfileEvaluation2,
|
|
1693
|
+
RELEASED_DOCUMENT_PROFILE,
|
|
1694
|
+
HISTORICAL_TESTIMONY_PROFILE,
|
|
1695
|
+
PHYSICAL_SAMPLE_PROFILE,
|
|
1696
|
+
INSTRUMENT_RESEARCH_PREREQUISITES_PROFILE
|
|
1697
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
1698
|
+
var META2 = 8 * 1024 * 1024;
|
|
1699
|
+
var TOTAL2 = 256 * 1024 * 1024;
|
|
1700
|
+
var FILES2 = 4096;
|
|
1701
|
+
var catalog = [
|
|
1702
|
+
{
|
|
1703
|
+
id: "instrument-research",
|
|
1704
|
+
title: "Instrument research",
|
|
1705
|
+
profile: INSTRUMENT_RESEARCH_PREREQUISITES_PROFILE,
|
|
1706
|
+
workflow: null,
|
|
1707
|
+
command: "disclosureos packet validate <packet.json>",
|
|
1708
|
+
useWhen: "Instrument observations with acquisition, calibration, timing and measurement records.",
|
|
1709
|
+
limits: "Checks documented research prerequisites; does not establish scientific validity."
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
id: "released-documents",
|
|
1713
|
+
title: "Released documents",
|
|
1714
|
+
profile: RELEASED_DOCUMENT_PROFILE,
|
|
1715
|
+
workflow: "released-documents:0.1.0",
|
|
1716
|
+
command: "disclosureos profile check <evaluation.json>",
|
|
1717
|
+
useWhen: "Explicitly selected public document sources and their direct extractions.",
|
|
1718
|
+
limits: "Does not authenticate releases or establish the truth of document contents."
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
id: "historical-testimony",
|
|
1722
|
+
title: "Historical testimony",
|
|
1723
|
+
profile: HISTORICAL_TESTIMONY_PROFILE,
|
|
1724
|
+
workflow: "historical-testimony:0.1.0",
|
|
1725
|
+
command: "disclosureos profile check <evaluation.json>",
|
|
1726
|
+
useWhen: "Selected testimony accounts, including restricted accounts and documented pseudonyms.",
|
|
1727
|
+
limits: "Does not assess witness credibility, firsthand knowledge or independent corroboration."
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
id: "physical-samples",
|
|
1731
|
+
title: "Physical samples",
|
|
1732
|
+
profile: PHYSICAL_SAMPLE_PROFILE,
|
|
1733
|
+
workflow: "physical-samples:0.1.0",
|
|
1734
|
+
command: "disclosureos profile check <evaluation.json>",
|
|
1735
|
+
useWhen: "Declared collected specimens with collection and custody records.",
|
|
1736
|
+
limits: "Does not cover derived samples or mixtures, or certify physical identity, custody truth, composition or origin."
|
|
1737
|
+
}
|
|
1738
|
+
];
|
|
1739
|
+
var safe = (value) => value.replace(
|
|
1740
|
+
/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,
|
|
1741
|
+
(c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
|
|
1742
|
+
);
|
|
1743
|
+
var line = (value) => console.log(safe(value));
|
|
1744
|
+
var label3 = (value) => {
|
|
1745
|
+
const words = value.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").toLowerCase();
|
|
1746
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
1747
|
+
};
|
|
1748
|
+
var status = (value) => ({
|
|
1749
|
+
passed: "passed",
|
|
1750
|
+
failed: "failed",
|
|
1751
|
+
not_checked: "not checked",
|
|
1752
|
+
satisfied: "satisfied",
|
|
1753
|
+
missing: "missing",
|
|
1754
|
+
not_applicable: "not applicable"
|
|
1755
|
+
})[value] ?? value;
|
|
1756
|
+
function emit2(output, jsonMode) {
|
|
1757
|
+
if (jsonMode) {
|
|
1758
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
line("DisclosureOS experimental profile checks");
|
|
1762
|
+
if (output.catalog) {
|
|
1763
|
+
for (const p of output.catalog) {
|
|
1764
|
+
line("");
|
|
1765
|
+
line(p.title);
|
|
1766
|
+
line(" Use for: " + p.useWhen);
|
|
1767
|
+
line(" Run: " + p.command);
|
|
1768
|
+
if (p.workflow) line(" Manifest workflow: " + p.workflow);
|
|
1769
|
+
line(" Limit: " + p.limits);
|
|
1770
|
+
}
|
|
1771
|
+
line("");
|
|
1772
|
+
line(
|
|
1773
|
+
"Choose the profile from the source records; no automatic selection or aggregate score is applied."
|
|
1774
|
+
);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (output.inspection) {
|
|
1778
|
+
const i = output.inspection;
|
|
1779
|
+
line("Selected: " + i.title);
|
|
1780
|
+
line("Workflow: " + i.workflow);
|
|
1781
|
+
line(
|
|
1782
|
+
`Declared files: ${i.assetCount} source files, ${i.dependencyCount} dependencies, history and selection`
|
|
1783
|
+
);
|
|
1784
|
+
line(
|
|
1785
|
+
"Manifest inspected. Input integrity and profile outcome: not checked. No receipt created."
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
const evaluation = output.evaluation;
|
|
1789
|
+
if (evaluation) {
|
|
1790
|
+
if (!output.inspection) {
|
|
1791
|
+
line(
|
|
1792
|
+
"Selected: " + (catalog.find((p2) => p2.workflow === output.selectedWorkflow)?.title ?? "Manifest not accepted")
|
|
1793
|
+
);
|
|
1794
|
+
if (output.selectedWorkflow) line("Workflow: " + output.selectedWorkflow);
|
|
1795
|
+
}
|
|
1796
|
+
line("Input integrity: " + status(evaluation.checks.external));
|
|
1797
|
+
line("Profile outcome: " + status(evaluation.checks.profile));
|
|
1798
|
+
line(
|
|
1799
|
+
"Receipt: " + (evaluation.receipt ? "created (unsigned)" : "not created")
|
|
1800
|
+
);
|
|
1801
|
+
const p = evaluation.profile;
|
|
1802
|
+
if (p) {
|
|
1803
|
+
const rows = "documents" in p ? p.documents : "accounts" in p ? p.accounts : p.samples;
|
|
1804
|
+
for (const row of rows) {
|
|
1805
|
+
line("");
|
|
1806
|
+
line(
|
|
1807
|
+
("sourceRef" in row ? row.sourceRef : row.sampleId) + ": " + status(row.status)
|
|
1808
|
+
);
|
|
1809
|
+
for (const r of row.requirements) {
|
|
1810
|
+
line(
|
|
1811
|
+
` ${r.importance === "required" ? "Required" : "Recommended"} | ${r.label}: ${status(r.status)}`
|
|
1812
|
+
);
|
|
1813
|
+
line(" " + r.reason);
|
|
1814
|
+
for (const issue of r.issues) line(" Next: " + issue.nextAction);
|
|
1815
|
+
if (r.blockedBy.length)
|
|
1816
|
+
line(" Blocked by: " + r.blockedBy.map(label3).join(", "));
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (!rows.length)
|
|
1820
|
+
for (const issue of p.issues) {
|
|
1821
|
+
line(issue.code + " " + issue.pointer + ": " + issue.message);
|
|
1822
|
+
line(" Next: " + issue.nextAction);
|
|
1823
|
+
}
|
|
1824
|
+
line("");
|
|
1825
|
+
line("Profile limits:");
|
|
1826
|
+
for (const [key, value] of Object.entries(p))
|
|
1827
|
+
if (value === "not_checked") line(" " + label3(key) + ": not checked");
|
|
1828
|
+
}
|
|
1829
|
+
if (!p)
|
|
1830
|
+
for (const issue of evaluation.issues)
|
|
1831
|
+
line(issue.code + " " + issue.pointer + ": " + issue.message);
|
|
1832
|
+
line(
|
|
1833
|
+
"A receipt preserves a result; it does not make a failed or unchecked profile pass."
|
|
1834
|
+
);
|
|
1835
|
+
line("Implementation and environment execution: not attested.");
|
|
1836
|
+
}
|
|
1837
|
+
for (const issue of output.issues) {
|
|
1838
|
+
line(issue.code + " " + issue.pointer + ": " + issue.message);
|
|
1839
|
+
line(" Next: " + issue.nextAction);
|
|
1840
|
+
}
|
|
1841
|
+
line(
|
|
1842
|
+
"Scientific eligibility: not checked. Use --json for the full result and receipt."
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
function directory3(path) {
|
|
1846
|
+
const s = lstatSync5(path);
|
|
1847
|
+
if (!s.isDirectory() || s.isSymbolicLink())
|
|
1848
|
+
throw Error("Expected a real directory.");
|
|
1849
|
+
}
|
|
1850
|
+
async function profile(args2) {
|
|
1851
|
+
if (args2.flags["help"]) {
|
|
1852
|
+
console.log(`DisclosureOS experimental profile checks
|
|
1853
|
+
|
|
1854
|
+
disclosureos profile prepare <plan.json> --out <new-directory> [--json]
|
|
1855
|
+
disclosureos profile list [--json]
|
|
1856
|
+
disclosureos profile inspect <evaluation.json> [--json]
|
|
1857
|
+
disclosureos profile check <evaluation.json> [--json]
|
|
1858
|
+
|
|
1859
|
+
The manifest explicitly selects released-documents:0.1.0, historical-testimony:0.1.0
|
|
1860
|
+
or physical-samples:0.1.0. Instrument research uses packet validate.
|
|
1861
|
+
Inspect reads only the manifest. Check reads history.json, selection.json,
|
|
1862
|
+
assets/<fileRef> and dependencies/<ref> beside it. No network access or supplied-code execution.
|
|
1863
|
+
Limits: 8 MiB metadata, 4096 input files, 256 MiB total bytes.
|
|
1864
|
+
Prepare writes a new private bundle from a profile_preparation plan; preparation
|
|
1865
|
+
success does not mean the profile passed. Other commands are read-only.
|
|
1866
|
+
Exit: 0 prepared/listed/inspected or profile passed with receipt, 1 failed/unchecked checks,
|
|
1867
|
+
2 usage or unreadable/malformed manifest. Full JSON may contain restricted metadata;
|
|
1868
|
+
apply access policy before sharing. No automatic profile selection or aggregate score.`);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
if (args2.subcommand === "prepare") {
|
|
1872
|
+
await prepareProfile(args2);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const output = {
|
|
1876
|
+
command: args2.subcommand,
|
|
1877
|
+
success: false,
|
|
1878
|
+
issues: [],
|
|
1879
|
+
scientificEligibility: "not_checked"
|
|
1880
|
+
};
|
|
1881
|
+
const finish = (code) => {
|
|
1882
|
+
emit2(output, !!args2.flags["json"]);
|
|
1883
|
+
if (code) process.exitCode = code;
|
|
1884
|
+
};
|
|
1885
|
+
if (!["list", "inspect", "check"].includes(args2.subcommand) || args2.positional.length !== (args2.subcommand === "list" ? 0 : 1) || Object.keys(args2.flags).some((f) => !["json", "help"].includes(f))) {
|
|
1886
|
+
output.issues.push({
|
|
1887
|
+
code: "CLI.USAGE",
|
|
1888
|
+
pointer: "",
|
|
1889
|
+
message: "Use profile list or profile inspect|check <evaluation.json> [--json].",
|
|
1890
|
+
nextAction: "Run disclosureos profile --help for explicit workflow and input layout."
|
|
1891
|
+
});
|
|
1892
|
+
finish(2);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
if (args2.subcommand === "list") {
|
|
1896
|
+
output.catalog = catalog;
|
|
1897
|
+
output.success = true;
|
|
1898
|
+
finish(0);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
let input, manifestBytes, path;
|
|
1902
|
+
try {
|
|
1903
|
+
path = resolve6(args2.positional[0]);
|
|
1904
|
+
manifestBytes = readLocal(path, META2);
|
|
1905
|
+
output.manifestSha256 = createHash5("sha256").update(manifestBytes).digest("hex");
|
|
1906
|
+
input = JSON.parse(
|
|
1907
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
1908
|
+
manifestBytes
|
|
1909
|
+
)
|
|
1910
|
+
);
|
|
1911
|
+
} catch {
|
|
1912
|
+
output.issues.push({
|
|
1913
|
+
code: "CLI.MANIFEST",
|
|
1914
|
+
pointer: "",
|
|
1915
|
+
message: "Cannot read the manifest as regular-file UTF-8 JSON within 8 MiB.",
|
|
1916
|
+
nextAction: "Supply a local evaluation manifest without a BOM or symlink; keep the file within the metadata limit."
|
|
1917
|
+
});
|
|
1918
|
+
finish(2);
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const parsed = ProfileEvaluationSchema.safeParse(input), preflight = await evaluateProfileEvaluation2(input);
|
|
1922
|
+
if (!parsed.success || preflight.checks.semantic === "failed") {
|
|
1923
|
+
output.evaluation = preflight;
|
|
1924
|
+
finish(1);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
output.selectedWorkflow = parsed.data.workflow;
|
|
1928
|
+
const manifest2 = parsed.data, dependencies = [
|
|
1929
|
+
...manifest2.dependencies.vocabularies,
|
|
1930
|
+
manifest2.dependencies.implementation,
|
|
1931
|
+
manifest2.dependencies.environment
|
|
1932
|
+
];
|
|
1933
|
+
const selected = catalog.find((p) => p.workflow === manifest2.workflow);
|
|
1934
|
+
if (args2.subcommand === "inspect") {
|
|
1935
|
+
output.inspection = {
|
|
1936
|
+
id: manifest2.id,
|
|
1937
|
+
workflow: manifest2.workflow,
|
|
1938
|
+
title: selected.title,
|
|
1939
|
+
profile: manifest2.workflow === "released-documents:0.1.0" ? RELEASED_DOCUMENT_PROFILE : manifest2.workflow === "historical-testimony:0.1.0" ? HISTORICAL_TESTIMONY_PROFILE : PHYSICAL_SAMPLE_PROFILE,
|
|
1940
|
+
assetCount: manifest2.assets.length,
|
|
1941
|
+
dependencyCount: dependencies.length,
|
|
1942
|
+
declaredInputBytes: manifest2.history.byteLength + manifest2.selection.byteLength + manifest2.assets.reduce((n, a) => n + a.byteLength, 0) + dependencies.reduce((n, d) => n + d.byteLength, 0),
|
|
1943
|
+
inputIntegrity: "not_checked",
|
|
1944
|
+
profileOutcome: "not_checked",
|
|
1945
|
+
receipt: "not_created"
|
|
1946
|
+
};
|
|
1947
|
+
output.success = true;
|
|
1948
|
+
finish(0);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (manifest2.assets.length + dependencies.length + 2 > FILES2 || manifestBytes.length + manifest2.history.byteLength + manifest2.selection.byteLength + manifest2.assets.reduce((n, a) => n + a.byteLength, 0) + dependencies.reduce((n, d) => n + d.byteLength, 0) > TOTAL2) {
|
|
1952
|
+
output.issues.push({
|
|
1953
|
+
code: "CLI.LIMIT",
|
|
1954
|
+
pointer: "",
|
|
1955
|
+
message: "Declared inputs exceed the 4096-file or 256 MiB budget.",
|
|
1956
|
+
nextAction: "Use a bounded evaluation input set; do not omit necessary records merely to make a profile pass."
|
|
1957
|
+
});
|
|
1958
|
+
finish(1);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const root = dirname5(path);
|
|
1962
|
+
let remaining = TOTAL2 - manifestBytes.length;
|
|
1963
|
+
try {
|
|
1964
|
+
directory3(root);
|
|
1965
|
+
} catch {
|
|
1966
|
+
output.issues.push({
|
|
1967
|
+
code: "CLI.DIRECTORY",
|
|
1968
|
+
pointer: "",
|
|
1969
|
+
message: "The bundle root must be a real directory.",
|
|
1970
|
+
nextAction: "Use a local directory without a symlink root."
|
|
1971
|
+
});
|
|
1972
|
+
finish(1);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const read = (relative, limit) => {
|
|
1976
|
+
try {
|
|
1977
|
+
const bytes2 = readLocal(join5(root, relative), Math.min(remaining, limit));
|
|
1978
|
+
remaining -= bytes2.length;
|
|
1979
|
+
return bytes2;
|
|
1980
|
+
} catch {
|
|
1981
|
+
output.issues.push({
|
|
1982
|
+
code: "CLI.INPUT_FILE",
|
|
1983
|
+
pointer: relative,
|
|
1984
|
+
message: "Cannot read this input as a regular file within its byte budget.",
|
|
1985
|
+
nextAction: "Provide the original pinned local file, respecting access restrictions; do not substitute another file."
|
|
1986
|
+
});
|
|
1987
|
+
return void 0;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
const historyBytes = read("history.json", META2), selectionBytes = read("selection.json", META2);
|
|
1991
|
+
const assetFiles = /* @__PURE__ */ new Map(), dependencyFiles = /* @__PURE__ */ new Map();
|
|
1992
|
+
for (const [name2, entries, map] of [
|
|
1993
|
+
["assets", manifest2.assets.map((a) => a.fileRef), assetFiles],
|
|
1994
|
+
["dependencies", dependencies.map((d) => d.ref), dependencyFiles]
|
|
1995
|
+
]) {
|
|
1996
|
+
try {
|
|
1997
|
+
directory3(join5(root, name2));
|
|
1998
|
+
} catch {
|
|
1999
|
+
output.issues.push({
|
|
2000
|
+
code: "CLI.DIRECTORY",
|
|
2001
|
+
pointer: name2,
|
|
2002
|
+
message: "Expected a real input directory.",
|
|
2003
|
+
nextAction: "Keep the assets and dependencies directories beside the manifest, including an empty assets directory when no files are declared."
|
|
2004
|
+
});
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
for (const ref of entries) {
|
|
2008
|
+
const bytes2 = read(name2 + "/" + ref, remaining);
|
|
2009
|
+
if (bytes2) map.set(ref, bytes2);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
output.evaluation = await evaluateProfileEvaluation2(input, {
|
|
2013
|
+
...historyBytes ? { historyBytes } : {},
|
|
2014
|
+
...selectionBytes ? { selectionBytes } : {},
|
|
2015
|
+
assetFiles,
|
|
2016
|
+
dependencyFiles
|
|
2017
|
+
});
|
|
2018
|
+
output.success = output.evaluation.success && output.issues.length === 0;
|
|
2019
|
+
finish(output.success ? 0 : 1);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/commands/migration-compatibility.ts
|
|
2023
|
+
import { createHash as createHash8 } from "crypto";
|
|
2024
|
+
import {
|
|
2025
|
+
MigrationCompatibilitySchema
|
|
2026
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
2027
|
+
import {
|
|
2028
|
+
parseAcquisitionContext
|
|
2029
|
+
} from "@disclosureos/instruments/experimental/v2";
|
|
2030
|
+
|
|
2031
|
+
// src/commands/migration-review.ts
|
|
2032
|
+
import { createHash as createHash7 } from "crypto";
|
|
2033
|
+
import {
|
|
2034
|
+
MigrationReviewSchema
|
|
2035
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
2036
|
+
import { parseExperimentalClaimHistory as parseExperimentalClaimHistory3 } from "@disclosureos/records/experimental/v2";
|
|
2037
|
+
|
|
2038
|
+
// src/commands/migration-plan.ts
|
|
2039
|
+
import { createHash as createHash6 } from "crypto";
|
|
2040
|
+
import { parseEnrichedObservation as parseEnrichedObservation2 } from "@disclosureos/schema";
|
|
2041
|
+
import {
|
|
2042
|
+
parseExperimentalClaimHistory as parseExperimentalClaimHistory2
|
|
2043
|
+
} from "@disclosureos/records/experimental/v2";
|
|
2044
|
+
var POLICY = "legacy-migration-dry-run:0.1.0";
|
|
2045
|
+
var sha256 = (bytes2) => createHash6("sha256").update(bytes2).digest("hex");
|
|
2046
|
+
var object2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2047
|
+
var escape = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2048
|
+
function fields(value) {
|
|
2049
|
+
const pending = [{ value, pointer: "", depth: 0 }];
|
|
2050
|
+
const leaves = [];
|
|
2051
|
+
let visited = 0;
|
|
2052
|
+
while (pending.length) {
|
|
2053
|
+
const item = pending.pop();
|
|
2054
|
+
if (++visited > 1e5 || item.depth > 64)
|
|
2055
|
+
throw new Error("Record exceeds 100000 nodes or depth 64");
|
|
2056
|
+
const entries = item.value !== null && typeof item.value === "object" ? Object.entries(item.value) : [];
|
|
2057
|
+
if (!entries.length) leaves.push(item.pointer);
|
|
2058
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
2059
|
+
const [key, value2] = entries[i];
|
|
2060
|
+
pending.push({
|
|
2061
|
+
value: value2,
|
|
2062
|
+
pointer: `${item.pointer}/${escape(key)}`,
|
|
2063
|
+
depth: item.depth + 1
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return leaves;
|
|
2068
|
+
}
|
|
2069
|
+
function mapping(pointer, candidate) {
|
|
2070
|
+
if (candidate && ["/id", "/createdAt", "/updatedAt", "/summary"].includes(pointer)) {
|
|
2071
|
+
return {
|
|
2072
|
+
pointer,
|
|
2073
|
+
disposition: "mapped",
|
|
2074
|
+
target: `/observation${pointer}`,
|
|
2075
|
+
reason: pointer === "/id" ? "Deterministic namespace and legacy ID hash; original ID retained in legacy data." : "Copied without inference."
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
return {
|
|
2079
|
+
pointer,
|
|
2080
|
+
disposition: "unresolved",
|
|
2081
|
+
reason: !candidate ? "Quarantined record retained; no candidate mapping applied." : pointer === "/status" ? "Candidate is a review draft; original publication status retained only in legacy data." : pointer.startsWith("/temporal/") ? "Time scale, precision, provenance and possible sentinel use require explicit review." : pointer.startsWith("/location/") ? "Reference frame, provenance and possible sentinel use require explicit review; zero is retained." : "Retained only in versioned legacy data; requires explicit mapping. Legacy evaluations are not v2 claims or comparable scores."
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function buildMigrationReport(bytes2, namespace) {
|
|
2085
|
+
const input = JSON.parse(
|
|
2086
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes2)
|
|
2087
|
+
);
|
|
2088
|
+
const rows = Array.isArray(input) ? input : [input];
|
|
2089
|
+
if (!rows.length || rows.length > 1e4)
|
|
2090
|
+
throw new Error("Input must contain 1 to 10000 records");
|
|
2091
|
+
const digest = sha256(bytes2);
|
|
2092
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2093
|
+
for (const row of rows) {
|
|
2094
|
+
if (object2(row) && typeof row.id === "string")
|
|
2095
|
+
counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
|
|
2096
|
+
}
|
|
2097
|
+
const records = rows.map((legacy, index) => {
|
|
2098
|
+
const sourcePointer = Array.isArray(input) ? `/${index}` : "";
|
|
2099
|
+
const reasons = [];
|
|
2100
|
+
let pointers = [];
|
|
2101
|
+
try {
|
|
2102
|
+
pointers = fields(legacy);
|
|
2103
|
+
} catch (error2) {
|
|
2104
|
+
reasons.push(error2.message);
|
|
2105
|
+
}
|
|
2106
|
+
if (object2(legacy) && typeof legacy.id === "string" && counts.get(legacy.id) > 1) {
|
|
2107
|
+
reasons.push(
|
|
2108
|
+
"Duplicate legacy ID in this input; all occurrences quarantined."
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
const validation = reasons.length ? void 0 : parseEnrichedObservation2(legacy);
|
|
2112
|
+
if (validation && !validation.success) {
|
|
2113
|
+
reasons.push(
|
|
2114
|
+
...validation.issues.map((issue) => `${issue.path}: ${issue.message}`)
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
let candidate;
|
|
2118
|
+
if (!reasons.length && object2(legacy)) {
|
|
2119
|
+
const id = `legacy-${sha256(JSON.stringify([namespace, legacy.id]))}`;
|
|
2120
|
+
const proposal = {
|
|
2121
|
+
kind: "claim_history",
|
|
2122
|
+
schemaVersion: "0.1.0",
|
|
2123
|
+
id: `${id}-history`,
|
|
2124
|
+
observation: {
|
|
2125
|
+
kind: "observation",
|
|
2126
|
+
schemaVersion: "0.1.0",
|
|
2127
|
+
id,
|
|
2128
|
+
status: "draft",
|
|
2129
|
+
createdAt: legacy.createdAt,
|
|
2130
|
+
updatedAt: legacy.updatedAt,
|
|
2131
|
+
...legacy.summary !== void 0 ? { summary: legacy.summary } : {},
|
|
2132
|
+
eventTime: { state: "unknown", reason: "unavailable" },
|
|
2133
|
+
position: { state: "unknown", reason: "unavailable" },
|
|
2134
|
+
sources: [
|
|
2135
|
+
{
|
|
2136
|
+
id: "legacy-input",
|
|
2137
|
+
kind: "other",
|
|
2138
|
+
access: "unknown",
|
|
2139
|
+
digest: { algorithm: "sha256", value: digest }
|
|
2140
|
+
}
|
|
2141
|
+
],
|
|
2142
|
+
products: [],
|
|
2143
|
+
methods: [],
|
|
2144
|
+
frames: [],
|
|
2145
|
+
assertions: [],
|
|
2146
|
+
measurements: [],
|
|
2147
|
+
processing: []
|
|
2148
|
+
},
|
|
2149
|
+
claims: []
|
|
2150
|
+
};
|
|
2151
|
+
const parsed = parseExperimentalClaimHistory2(proposal);
|
|
2152
|
+
if (parsed.success) candidate = parsed.data;
|
|
2153
|
+
else
|
|
2154
|
+
reasons.push(
|
|
2155
|
+
...parsed.issues.map((issue) => `${issue.pointer}: ${issue.message}`)
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
sourcePointer,
|
|
2160
|
+
sourceId: object2(legacy) && typeof legacy.id === "string" ? legacy.id : null,
|
|
2161
|
+
decision: candidate ? "candidate_requires_review" : "quarantined",
|
|
2162
|
+
legacy: {
|
|
2163
|
+
format: "disclosureos-v1-input",
|
|
2164
|
+
validation: validation?.success ? "valid" : validation ? "invalid" : "not_checked",
|
|
2165
|
+
// Do not re-serialize an over-depth tree; exact bytes and row pointer retain it.
|
|
2166
|
+
...pointers.length ? { data: legacy } : { retainedIn: "source.bytes" }
|
|
2167
|
+
},
|
|
2168
|
+
...candidate ? { candidate } : {},
|
|
2169
|
+
reasons,
|
|
2170
|
+
mappingComplete: pointers.length > 0,
|
|
2171
|
+
mapping: pointers.map(
|
|
2172
|
+
(pointer) => mapping(pointer, candidate !== void 0)
|
|
2173
|
+
)
|
|
2174
|
+
};
|
|
2175
|
+
});
|
|
2176
|
+
const candidates = records.filter(
|
|
2177
|
+
(row) => row.decision === "candidate_requires_review"
|
|
2178
|
+
).length;
|
|
2179
|
+
return {
|
|
2180
|
+
kind: "legacy_migration_dry_run",
|
|
2181
|
+
schemaVersion: "0.1.0",
|
|
2182
|
+
policy: POLICY,
|
|
2183
|
+
namespace,
|
|
2184
|
+
source: {
|
|
2185
|
+
sha256: digest,
|
|
2186
|
+
byteLength: bytes2.byteLength,
|
|
2187
|
+
encoding: "base64",
|
|
2188
|
+
bytes: Buffer.from(bytes2).toString("base64")
|
|
2189
|
+
},
|
|
2190
|
+
counts: {
|
|
2191
|
+
input: rows.length,
|
|
2192
|
+
candidates,
|
|
2193
|
+
quarantined: rows.length - candidates,
|
|
2194
|
+
migrated: 0
|
|
2195
|
+
},
|
|
2196
|
+
records
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/commands/migration-review.ts
|
|
2201
|
+
var hash3 = (bytes2) => createHash7("sha256").update(bytes2).digest("hex");
|
|
2202
|
+
function decimalKey(literal) {
|
|
2203
|
+
const [mantissa = "", exponent = "0"] = literal.toLowerCase().split("e");
|
|
2204
|
+
if (exponent.replace(/^[+-]/, "").length > 6)
|
|
2205
|
+
throw new Error("JSON numeric exponent exceeds review budget");
|
|
2206
|
+
const negative = mantissa.startsWith("-");
|
|
2207
|
+
const unsigned = negative ? mantissa.slice(1) : mantissa;
|
|
2208
|
+
const [whole = "", fraction = ""] = unsigned.split(".");
|
|
2209
|
+
let digits = (whole + fraction).replace(/^0+/, "");
|
|
2210
|
+
if (!digits) return "0";
|
|
2211
|
+
const withoutTrailing = digits.replace(/0+$/, "");
|
|
2212
|
+
const scale = Number(exponent) - fraction.length + digits.length - withoutTrailing.length;
|
|
2213
|
+
digits = withoutTrailing;
|
|
2214
|
+
return `${negative ? "-" : ""}${digits}e${scale}`;
|
|
2215
|
+
}
|
|
2216
|
+
function reviewedJson(bytes2) {
|
|
2217
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
|
|
2218
|
+
const value = JSON.parse(text);
|
|
2219
|
+
const stack = [];
|
|
2220
|
+
for (let i = 0; i < text.length; ) {
|
|
2221
|
+
const char = text[i];
|
|
2222
|
+
if (char === "{" || char === "[") {
|
|
2223
|
+
stack.push(char === "{" ? /* @__PURE__ */ new Set() : null);
|
|
2224
|
+
if (stack.length > 64) throw new Error("Reviewed JSON exceeds depth 64");
|
|
2225
|
+
i++;
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (char === "}" || char === "]") {
|
|
2229
|
+
stack.pop();
|
|
2230
|
+
i++;
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
if (char === '"') {
|
|
2234
|
+
const start = i++;
|
|
2235
|
+
while (i < text.length) {
|
|
2236
|
+
if (text[i] === "\\") {
|
|
2237
|
+
i += 2;
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (text[i++] === '"') break;
|
|
2241
|
+
}
|
|
2242
|
+
let after = i;
|
|
2243
|
+
while (/\s/.test(text[after] ?? "") && after < text.length) after++;
|
|
2244
|
+
if (text[after] === ":") {
|
|
2245
|
+
const key = JSON.parse(text.slice(start, i));
|
|
2246
|
+
const keys = stack[stack.length - 1];
|
|
2247
|
+
if (keys?.has(key))
|
|
2248
|
+
throw new Error(
|
|
2249
|
+
"Duplicate JSON object key; reviewed mapping requires unambiguous input"
|
|
2250
|
+
);
|
|
2251
|
+
keys?.add(key);
|
|
2252
|
+
}
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
if (char === "-" || char >= "0" && char <= "9") {
|
|
2256
|
+
const start = i++;
|
|
2257
|
+
while (i < text.length && !/[\s,\]}]/.test(text[i])) i++;
|
|
2258
|
+
const literal = text.slice(start, i);
|
|
2259
|
+
const number = Number(literal);
|
|
2260
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number) || Object.is(number, -0) || decimalKey(literal) !== decimalKey(String(number))) {
|
|
2261
|
+
throw new Error(
|
|
2262
|
+
"Unsafe JSON numeric representation; reviewed mapping requires explicit source normalization"
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
i++;
|
|
2268
|
+
}
|
|
2269
|
+
return value;
|
|
2270
|
+
}
|
|
2271
|
+
function applyDecisions(row, decisions, plan, digest) {
|
|
2272
|
+
if (!decisions.length) return { ...row, review: { status: "not_requested" } };
|
|
2273
|
+
if (!row.candidate)
|
|
2274
|
+
return {
|
|
2275
|
+
...row,
|
|
2276
|
+
review: { status: "rejected", reason: "Base row is quarantined." }
|
|
2277
|
+
};
|
|
2278
|
+
const candidate = structuredClone(row.candidate);
|
|
2279
|
+
const issues = [];
|
|
2280
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
2281
|
+
const observation = { ...candidate.observation };
|
|
2282
|
+
for (const decision of decisions) {
|
|
2283
|
+
const prefix = decision.field === "eventTime" ? "/temporal/" : "/location/";
|
|
2284
|
+
for (const pointer of decision.sourcePointers) {
|
|
2285
|
+
if (!pointer.startsWith(prefix) || !row.mapping.some((field) => field.pointer === pointer)) {
|
|
2286
|
+
issues.push(`Unknown or out-of-domain source pointer: ${pointer}`);
|
|
2287
|
+
}
|
|
2288
|
+
mapped.set(pointer, {
|
|
2289
|
+
target: `/observation/${decision.field}`,
|
|
2290
|
+
reason: decision.rationale
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
observation[decision.field] = decision.value;
|
|
2294
|
+
if (decision.field === "position") observation.frames = decision.frames;
|
|
2295
|
+
}
|
|
2296
|
+
observation.extensions = {
|
|
2297
|
+
"disclosureos.migration": {
|
|
2298
|
+
policy: "legacy-migration-review:0.1.0",
|
|
2299
|
+
reviewSha256: digest,
|
|
2300
|
+
sourceId: row.sourceId,
|
|
2301
|
+
sourcePointer: row.sourcePointer,
|
|
2302
|
+
reviewedBy: plan.reviewedBy,
|
|
2303
|
+
reviewedAt: plan.reviewedAt,
|
|
2304
|
+
decisions,
|
|
2305
|
+
reviewerIdentity: "not_authenticated",
|
|
2306
|
+
scientificValidity: "not_checked"
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
const validated = parseExperimentalClaimHistory3({
|
|
2310
|
+
...candidate,
|
|
2311
|
+
observation
|
|
2312
|
+
});
|
|
2313
|
+
if (!validated.success)
|
|
2314
|
+
issues.push(
|
|
2315
|
+
...validated.issues.map((issue) => `${issue.pointer}: ${issue.message}`)
|
|
2316
|
+
);
|
|
2317
|
+
if (issues.length || !validated.success) {
|
|
2318
|
+
const { candidate: omitted, ...rest } = row;
|
|
2319
|
+
void omitted;
|
|
2320
|
+
return {
|
|
2321
|
+
...rest,
|
|
2322
|
+
decision: "quarantined",
|
|
2323
|
+
reasons: [...row.reasons, ...issues],
|
|
2324
|
+
mapping: row.mapping.map((field) => ({
|
|
2325
|
+
pointer: field.pointer,
|
|
2326
|
+
disposition: "unresolved",
|
|
2327
|
+
reason: "Reviewed mapping rejected; no candidate emitted."
|
|
2328
|
+
})),
|
|
2329
|
+
review: { status: "rejected" }
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
...row,
|
|
2334
|
+
candidate: validated.data,
|
|
2335
|
+
mapping: row.mapping.map(
|
|
2336
|
+
(field) => mapped.has(field.pointer) ? {
|
|
2337
|
+
pointer: field.pointer,
|
|
2338
|
+
disposition: "mapped",
|
|
2339
|
+
...mapped.get(field.pointer)
|
|
2340
|
+
} : field
|
|
2341
|
+
),
|
|
2342
|
+
review: {
|
|
2343
|
+
status: "applied",
|
|
2344
|
+
reviewerIdentity: "not_authenticated",
|
|
2345
|
+
scientificValidity: "not_checked"
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
function reviewMigration(args2) {
|
|
2350
|
+
if (args2.flags["help"]) {
|
|
2351
|
+
console.log(
|
|
2352
|
+
"disclosureos migrate review <legacy.json> <review.json> [--json]\nApply explicit time/position decisions to review drafts; no imports or file writes."
|
|
2353
|
+
);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
try {
|
|
2357
|
+
if (args2.positional.length !== 2 || Object.keys(args2.flags).some((key) => key !== "json")) {
|
|
2358
|
+
throw new Error(
|
|
2359
|
+
"Usage: disclosureos migrate review <legacy.json> <review.json> [--json]"
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
const reviewBytes = readLocal(args2.positional[1], 2 * 1024 * 1024);
|
|
2363
|
+
const source = readLocal(args2.positional[0], 8 * 1024 * 1024);
|
|
2364
|
+
const report = buildReviewedMigration(source, reviewBytes);
|
|
2365
|
+
if (args2.flags["json"]) console.log(JSON.stringify(report, null, 2));
|
|
2366
|
+
else {
|
|
2367
|
+
console.log(
|
|
2368
|
+
`Experimental migration review: ${report.counts.input} inputs, ${report.counts.candidates} candidates, ${report.counts.quarantined} quarantined, 0 migrated.`
|
|
2369
|
+
);
|
|
2370
|
+
for (const row of report.records)
|
|
2371
|
+
console.log(
|
|
2372
|
+
JSON.stringify({
|
|
2373
|
+
sourceId: row.sourceId,
|
|
2374
|
+
decision: row.decision,
|
|
2375
|
+
review: row.review,
|
|
2376
|
+
unresolvedFields: row.mapping.filter(
|
|
2377
|
+
(field) => field.disposition === "unresolved"
|
|
2378
|
+
).length,
|
|
2379
|
+
reasons: row.reasons
|
|
2380
|
+
})
|
|
2381
|
+
);
|
|
2382
|
+
console.log(
|
|
2383
|
+
"Review identity and scientific validity remain unchecked. Use --json for pinned inputs, decisions and candidate mappings."
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
if (report.counts.quarantined) process.exitCode = 1;
|
|
2387
|
+
} catch (error2) {
|
|
2388
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2389
|
+
if (args2.flags["json"])
|
|
2390
|
+
console.log(
|
|
2391
|
+
JSON.stringify({
|
|
2392
|
+
success: false,
|
|
2393
|
+
stage: "review_input",
|
|
2394
|
+
error: message
|
|
2395
|
+
})
|
|
2396
|
+
);
|
|
2397
|
+
else console.error(message);
|
|
2398
|
+
process.exitCode = 2;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function buildReviewedMigration(source, reviewBytes) {
|
|
2402
|
+
const parsed = MigrationReviewSchema.safeParse(reviewedJson(reviewBytes));
|
|
2403
|
+
if (!parsed.success)
|
|
2404
|
+
throw new Error(`Invalid review plan: ${parsed.error.message}`);
|
|
2405
|
+
const plan = parsed.data;
|
|
2406
|
+
if (hash3(source) !== plan.source.sha256 || source.byteLength !== plan.source.byteLength)
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
"Source byte pin mismatch; regenerate and review decisions for the exact input"
|
|
2409
|
+
);
|
|
2410
|
+
reviewedJson(source);
|
|
2411
|
+
const base = buildMigrationReport(source, plan.namespace);
|
|
2412
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2413
|
+
const sourceIds = new Set(base.records.map((row) => row.sourceId));
|
|
2414
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2415
|
+
for (const decision of plan.decisions) {
|
|
2416
|
+
const key = JSON.stringify([decision.sourceId, decision.field]);
|
|
2417
|
+
if (seen.has(key))
|
|
2418
|
+
throw new Error("Duplicate decision for source ID and field");
|
|
2419
|
+
seen.add(key);
|
|
2420
|
+
const group = byId.get(decision.sourceId) ?? [];
|
|
2421
|
+
group.push(decision);
|
|
2422
|
+
byId.set(decision.sourceId, group);
|
|
2423
|
+
if (!sourceIds.has(decision.sourceId))
|
|
2424
|
+
throw new Error("Review decision names an unknown source ID");
|
|
2425
|
+
if (new Set(decision.sourcePointers).size !== decision.sourcePointers.length)
|
|
2426
|
+
throw new Error("Duplicate source pointer in review decision");
|
|
2427
|
+
}
|
|
2428
|
+
const digest = hash3(reviewBytes);
|
|
2429
|
+
const records = base.records.map(
|
|
2430
|
+
(row) => applyDecisions(row, byId.get(row.sourceId ?? "") ?? [], plan, digest)
|
|
2431
|
+
);
|
|
2432
|
+
const candidates = records.filter(
|
|
2433
|
+
(row) => row.decision === "candidate_requires_review"
|
|
2434
|
+
).length;
|
|
2435
|
+
return {
|
|
2436
|
+
...base,
|
|
2437
|
+
kind: "legacy_migration_review_result",
|
|
2438
|
+
policy: "legacy-migration-review:0.1.0",
|
|
2439
|
+
review: {
|
|
2440
|
+
sha256: digest,
|
|
2441
|
+
byteLength: reviewBytes.byteLength,
|
|
2442
|
+
encoding: "base64",
|
|
2443
|
+
bytes: Buffer.from(reviewBytes).toString("base64"),
|
|
2444
|
+
reviewerIdentity: "not_authenticated"
|
|
2445
|
+
},
|
|
2446
|
+
records,
|
|
2447
|
+
counts: {
|
|
2448
|
+
input: records.length,
|
|
2449
|
+
candidates,
|
|
2450
|
+
quarantined: records.length - candidates,
|
|
2451
|
+
migrated: 0
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// src/commands/migration-compatibility.ts
|
|
2457
|
+
var hash4 = (b) => createHash8("sha256").update(b).digest("hex");
|
|
2458
|
+
function unpack(blob) {
|
|
2459
|
+
const bytes2 = Buffer.from(blob.bytes, "base64");
|
|
2460
|
+
if (bytes2.toString("base64") !== blob.bytes || bytes2.length !== blob.byteLength || hash4(bytes2) !== blob.sha256)
|
|
2461
|
+
throw new Error(
|
|
2462
|
+
"Embedded artifact byte pin mismatch or noncanonical base64"
|
|
2463
|
+
);
|
|
2464
|
+
return bytes2;
|
|
2465
|
+
}
|
|
2466
|
+
var fieldPolicy = (pointer) => {
|
|
2467
|
+
const root = pointer.split("/")[1];
|
|
2468
|
+
if (["temporal", "location"].includes(root ?? ""))
|
|
2469
|
+
return {
|
|
2470
|
+
disposition: "unresolved",
|
|
2471
|
+
reason: "Explicit time/frame review is available through migrate review; no sentinel or datum inference."
|
|
2472
|
+
};
|
|
2473
|
+
if (root === "sensorEvidence")
|
|
2474
|
+
return {
|
|
2475
|
+
disposition: "unresolved",
|
|
2476
|
+
reason: "Sensor revision, acquisition, units and measurement provenance require explicit review; bare sensor labels and calibrated booleans are insufficient."
|
|
2477
|
+
};
|
|
2478
|
+
if ([
|
|
2479
|
+
"observableAssessments",
|
|
2480
|
+
"origin",
|
|
2481
|
+
"investigation",
|
|
2482
|
+
"testimony",
|
|
2483
|
+
"physicalEvidence",
|
|
2484
|
+
"documents",
|
|
2485
|
+
"provenance",
|
|
2486
|
+
"media",
|
|
2487
|
+
"featuredMedia",
|
|
2488
|
+
"internalNotes",
|
|
2489
|
+
"extensions",
|
|
2490
|
+
"status",
|
|
2491
|
+
"schemaVersion",
|
|
2492
|
+
"identifiers",
|
|
2493
|
+
"dataSourceId",
|
|
2494
|
+
"description",
|
|
2495
|
+
"objectCharacteristics",
|
|
2496
|
+
"eventType",
|
|
2497
|
+
"sourceData",
|
|
2498
|
+
"movement",
|
|
2499
|
+
"witnesses",
|
|
2500
|
+
"responseImpact",
|
|
2501
|
+
"environment",
|
|
2502
|
+
"relations",
|
|
2503
|
+
"aviation"
|
|
2504
|
+
].includes(root ?? ""))
|
|
2505
|
+
return {
|
|
2506
|
+
disposition: "retained_without_conversion",
|
|
2507
|
+
reason: "Retained in the original legacy envelope; no automatic v2 assertion, claim, measurement, publication or evaluation conversion is supported."
|
|
2508
|
+
};
|
|
2509
|
+
return {
|
|
2510
|
+
disposition: "unresolved",
|
|
2511
|
+
reason: "No supported conversion for this field; original bytes remain authoritative."
|
|
2512
|
+
};
|
|
2513
|
+
};
|
|
2514
|
+
function compatibilityReport(source, namespace, reviewBytes) {
|
|
2515
|
+
reviewedJson(source);
|
|
2516
|
+
const base = buildMigrationReport(source, namespace);
|
|
2517
|
+
let plan;
|
|
2518
|
+
let context;
|
|
2519
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
2520
|
+
const rowsById = /* @__PURE__ */ new Map();
|
|
2521
|
+
for (const row of base.records) {
|
|
2522
|
+
const group = rowsById.get(row.sourceId) ?? [];
|
|
2523
|
+
group.push(row);
|
|
2524
|
+
rowsById.set(row.sourceId, group);
|
|
2525
|
+
}
|
|
2526
|
+
if (reviewBytes) {
|
|
2527
|
+
plan = MigrationCompatibilitySchema.parse(reviewedJson(reviewBytes));
|
|
2528
|
+
if (plan.namespace !== namespace || plan.source.sha256 !== base.source.sha256 || plan.source.byteLength !== source.length)
|
|
2529
|
+
throw new Error("Sensor review namespace or source pin mismatch");
|
|
2530
|
+
context = parseAcquisitionContext(reviewedJson(unpack(plan.context)));
|
|
2531
|
+
if (!context.success)
|
|
2532
|
+
throw new Error(
|
|
2533
|
+
"Invalid acquisition context: " + context.issues.map((i) => i.message).join("; ")
|
|
2534
|
+
);
|
|
2535
|
+
for (const d of plan.decisions) {
|
|
2536
|
+
const key = JSON.stringify([d.sourceId, d.sensorPointer]);
|
|
2537
|
+
if (decisions.has(key))
|
|
2538
|
+
throw new Error("Duplicate sensor revision decision");
|
|
2539
|
+
const rows = rowsById.get(d.sourceId) ?? [];
|
|
2540
|
+
if (rows.length !== 1 || !rows[0].candidate)
|
|
2541
|
+
throw new Error("Sensor review requires a unique, valid legacy row");
|
|
2542
|
+
if (!rows[0].mapping.some((f) => f.pointer === d.sensorPointer))
|
|
2543
|
+
throw new Error("Unknown legacy sensor pointer");
|
|
2544
|
+
decisions.set(key, d);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
const acquisitions = new Map(
|
|
2548
|
+
context?.success ? context.data.acquisitions.map(
|
|
2549
|
+
(a) => [`acquisition:${a.id}`, a]
|
|
2550
|
+
) : []
|
|
2551
|
+
);
|
|
2552
|
+
const manifests = new Map(
|
|
2553
|
+
context?.success ? context.data.manifests.map((m) => [`manifest:${m.id}`, m]) : []
|
|
2554
|
+
);
|
|
2555
|
+
const records = base.records.map((row) => {
|
|
2556
|
+
const sensors2 = row.mapping.filter(
|
|
2557
|
+
(f) => /^\/sensorEvidence\/sensors\/(?:0|[1-9][0-9]*)\/sensorRef$/.test(
|
|
2558
|
+
f.pointer
|
|
2559
|
+
)
|
|
2560
|
+
).map((f) => {
|
|
2561
|
+
const d = decisions.get(JSON.stringify([row.sourceId, f.pointer]));
|
|
2562
|
+
const unresolved = {
|
|
2563
|
+
sourcePointer: f.pointer,
|
|
2564
|
+
status: "unresolved"
|
|
2565
|
+
};
|
|
2566
|
+
if (!d)
|
|
2567
|
+
return {
|
|
2568
|
+
...unresolved,
|
|
2569
|
+
reason: "No explicit revision review supplied."
|
|
2570
|
+
};
|
|
2571
|
+
try {
|
|
2572
|
+
if (!("data" in row.legacy))
|
|
2573
|
+
throw new Error("Legacy row data unavailable");
|
|
2574
|
+
const data = row.legacy.data;
|
|
2575
|
+
const sensor = data.sensorEvidence.sensors[Number(f.pointer.split("/")[3])];
|
|
2576
|
+
if (sensor?.sensorRef !== d.legacyRef)
|
|
2577
|
+
throw new Error("Declared legacy reference differs from source");
|
|
2578
|
+
const acquisition = acquisitions.get(d.acquisitionRef);
|
|
2579
|
+
if (!acquisition || acquisition.manifest.state !== "pinned")
|
|
2580
|
+
throw new Error(
|
|
2581
|
+
"Acquisition must explicitly pin a manifest; no automatic revision selection"
|
|
2582
|
+
);
|
|
2583
|
+
const manifestRef = acquisition.manifest.ref;
|
|
2584
|
+
const manifest2 = manifests.get(manifestRef);
|
|
2585
|
+
if (!manifest2) throw new Error("Pinned manifest is unavailable");
|
|
2586
|
+
const manifestBytes = unpack(d.manifest);
|
|
2587
|
+
unpack(d.provenance);
|
|
2588
|
+
if (hash4(manifestBytes) !== manifest2.artifact.digest.value || manifestBytes.length !== manifest2.artifact.byteLength)
|
|
2589
|
+
throw new Error("Manifest bytes differ from acquisition revision");
|
|
2590
|
+
return {
|
|
2591
|
+
sourcePointer: f.pointer,
|
|
2592
|
+
status: "reviewed_revision_pin",
|
|
2593
|
+
legacyRef: d.legacyRef,
|
|
2594
|
+
acquisitionRef: d.acquisitionRef,
|
|
2595
|
+
instrumentRef: acquisition.instrumentRef,
|
|
2596
|
+
manifestRef,
|
|
2597
|
+
manifestVersion: manifest2.version,
|
|
2598
|
+
manifest: {
|
|
2599
|
+
sha256: d.manifest.sha256,
|
|
2600
|
+
byteLength: d.manifest.byteLength
|
|
2601
|
+
},
|
|
2602
|
+
provenance: {
|
|
2603
|
+
sha256: d.provenance.sha256,
|
|
2604
|
+
byteLength: d.provenance.byteLength,
|
|
2605
|
+
locator: d.provenanceLocator
|
|
2606
|
+
},
|
|
2607
|
+
rationale: d.rationale,
|
|
2608
|
+
associationSupport: "reviewer_declared_not_verified",
|
|
2609
|
+
calibration: "not_checked",
|
|
2610
|
+
measurementConversion: "not_performed"
|
|
2611
|
+
};
|
|
2612
|
+
} catch (error2) {
|
|
2613
|
+
return {
|
|
2614
|
+
...unresolved,
|
|
2615
|
+
reason: error2 instanceof Error ? error2.message : String(error2)
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
const fields2 = row.mapping.map((f) => ({
|
|
2620
|
+
pointer: f.pointer,
|
|
2621
|
+
...f.disposition === "mapped" ? { disposition: "mapped", target: f.target, reason: f.reason } : row.candidate ? fieldPolicy(f.pointer) : {
|
|
2622
|
+
disposition: "unresolved",
|
|
2623
|
+
reason: "Source row quarantined; no conversion supported."
|
|
2624
|
+
}
|
|
2625
|
+
}));
|
|
2626
|
+
return {
|
|
2627
|
+
sourcePointer: row.sourcePointer,
|
|
2628
|
+
sourceId: row.sourceId,
|
|
2629
|
+
rowStatus: row.decision,
|
|
2630
|
+
reasons: row.reasons,
|
|
2631
|
+
mappingComplete: row.mappingComplete,
|
|
2632
|
+
fields: fields2,
|
|
2633
|
+
sensors: sensors2
|
|
2634
|
+
};
|
|
2635
|
+
});
|
|
2636
|
+
const allFields = records.flatMap((r) => r.fields), sensors = records.flatMap((r) => r.sensors);
|
|
2637
|
+
return {
|
|
2638
|
+
kind: "legacy_migration_compatibility_report",
|
|
2639
|
+
schemaVersion: "0.1.0",
|
|
2640
|
+
policy: "legacy-migration-compatibility:0.1.0",
|
|
2641
|
+
namespace,
|
|
2642
|
+
source: base.source,
|
|
2643
|
+
...plan && reviewBytes ? {
|
|
2644
|
+
review: {
|
|
2645
|
+
sha256: hash4(reviewBytes),
|
|
2646
|
+
byteLength: reviewBytes.length,
|
|
2647
|
+
encoding: "base64",
|
|
2648
|
+
bytes: Buffer.from(reviewBytes).toString("base64"),
|
|
2649
|
+
reviewedBy: plan.reviewedBy,
|
|
2650
|
+
reviewedAt: plan.reviewedAt,
|
|
2651
|
+
reviewerIdentity: "not_authenticated"
|
|
2652
|
+
}
|
|
2653
|
+
} : {},
|
|
2654
|
+
records,
|
|
2655
|
+
counts: {
|
|
2656
|
+
input: records.length,
|
|
2657
|
+
candidates: base.counts.candidates,
|
|
2658
|
+
quarantined: base.counts.quarantined,
|
|
2659
|
+
fields: allFields.length,
|
|
2660
|
+
mapped: allFields.filter((f) => f.disposition === "mapped").length,
|
|
2661
|
+
retainedWithoutConversion: allFields.filter(
|
|
2662
|
+
(f) => f.disposition === "retained_without_conversion"
|
|
2663
|
+
).length,
|
|
2664
|
+
unresolved: allFields.filter((f) => f.disposition === "unresolved").length,
|
|
2665
|
+
sensorReferences: sensors.length,
|
|
2666
|
+
reviewedRevisionPins: sensors.filter(
|
|
2667
|
+
(s) => s.status === "reviewed_revision_pin"
|
|
2668
|
+
).length,
|
|
2669
|
+
unresolvedSensorReferences: sensors.filter(
|
|
2670
|
+
(s) => s.status === "unresolved"
|
|
2671
|
+
).length,
|
|
2672
|
+
indexImported: 0
|
|
2673
|
+
},
|
|
2674
|
+
scientificValidity: "not_checked",
|
|
2675
|
+
applicationChanges: "none",
|
|
2676
|
+
scope: "compatibility_inventory_and_declared_revision_review"
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
function migrationCompatibility(args2) {
|
|
2680
|
+
const usage2 = "migrate compatibility <legacy.json> [sensor-review.json] --id <namespace> [--json]";
|
|
2681
|
+
if (args2.flags["help"]) {
|
|
2682
|
+
console.log(usage2);
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
try {
|
|
2686
|
+
const namespace = args2.flags["id"];
|
|
2687
|
+
if (![1, 2].includes(args2.positional.length) || typeof namespace !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(namespace) || Object.keys(args2.flags).some((k) => !["id", "json"].includes(k)))
|
|
2688
|
+
throw new Error(usage2);
|
|
2689
|
+
const report = compatibilityReport(
|
|
2690
|
+
readLocal(args2.positional[0], 8 * 1024 * 1024),
|
|
2691
|
+
namespace,
|
|
2692
|
+
args2.positional[1] ? readLocal(args2.positional[1], 2 * 1024 * 1024) : void 0
|
|
2693
|
+
);
|
|
2694
|
+
if (args2.flags["json"]) console.log(JSON.stringify(report, null, 2));
|
|
2695
|
+
else
|
|
2696
|
+
console.log(
|
|
2697
|
+
`Compatibility: ${report.counts.input} rows; ${report.counts.mapped} mapped fields, ${report.counts.retainedWithoutConversion} retained without conversion, ${report.counts.unresolved} unresolved.
|
|
2698
|
+
Sensor references: ${report.counts.reviewedRevisionPins} reviewed pins, ${report.counts.unresolvedSensorReferences} unresolved. No application changes.`
|
|
2699
|
+
);
|
|
2700
|
+
if (report.counts.quarantined || report.counts.unresolved || report.counts.unresolvedSensorReferences)
|
|
2701
|
+
process.exitCode = 1;
|
|
2702
|
+
} catch (error2) {
|
|
2703
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2704
|
+
if (args2.flags["json"])
|
|
2705
|
+
console.log(
|
|
2706
|
+
JSON.stringify({
|
|
2707
|
+
success: false,
|
|
2708
|
+
stage: "migration_compatibility",
|
|
2709
|
+
error: message
|
|
2710
|
+
})
|
|
2711
|
+
);
|
|
2712
|
+
else console.error(message);
|
|
2713
|
+
process.exitCode = 2;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// src/commands/migration-legacy-scores.ts
|
|
2718
|
+
import { createHash as createHash9 } from "crypto";
|
|
2719
|
+
import { MigrationLegacyScoresSchema } from "@disclosureos/schema/experimental/v2";
|
|
2720
|
+
import {
|
|
2721
|
+
CompletenessResultSchema,
|
|
2722
|
+
ScoreResultSchema,
|
|
2723
|
+
SCORING_SCHEMA_VERSION
|
|
2724
|
+
} from "@disclosureos/scoring";
|
|
2725
|
+
var hash5 = (bytes2) => createHash9("sha256").update(bytes2).digest("hex");
|
|
2726
|
+
var archive = (bytes2) => ({
|
|
2727
|
+
sha256: hash5(bytes2),
|
|
2728
|
+
byteLength: bytes2.byteLength,
|
|
2729
|
+
encoding: "base64",
|
|
2730
|
+
bytes: Buffer.from(bytes2).toString("base64")
|
|
2731
|
+
});
|
|
2732
|
+
function preserveLegacyScores(source, planBytes) {
|
|
2733
|
+
const plan = MigrationLegacyScoresSchema.parse(reviewedJson(planBytes));
|
|
2734
|
+
const sourceDigest = hash5(source), reviewDigest = hash5(planBytes);
|
|
2735
|
+
if (sourceDigest !== plan.source.sha256 || source.byteLength !== plan.source.byteLength)
|
|
2736
|
+
throw new Error("Legacy score source byte pin mismatch");
|
|
2737
|
+
const input = reviewedJson(source);
|
|
2738
|
+
const rows = Array.isArray(input) ? input : [input];
|
|
2739
|
+
if (!rows.length || rows.length > 1e4)
|
|
2740
|
+
throw new Error("Legacy score source requires 1 to 10000 rows");
|
|
2741
|
+
const pointers = rows.map((_, i) => Array.isArray(input) ? `/${i}` : "");
|
|
2742
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
2743
|
+
const knownPointers = new Set(pointers);
|
|
2744
|
+
for (const decision of plan.decisions) {
|
|
2745
|
+
if (!knownPointers.has(decision.sourcePointer))
|
|
2746
|
+
throw new Error("Decision names an unknown whole-row source pointer");
|
|
2747
|
+
if (decisions.has(decision.sourcePointer))
|
|
2748
|
+
throw new Error("Duplicate decision for legacy score row");
|
|
2749
|
+
decisions.set(decision.sourcePointer, decision);
|
|
2750
|
+
}
|
|
2751
|
+
const records = rows.map((data, index) => {
|
|
2752
|
+
const sourcePointer = pointers[index];
|
|
2753
|
+
const decision = decisions.get(sourcePointer);
|
|
2754
|
+
const legacy = { format: "standalone_legacy_score_input", data };
|
|
2755
|
+
if (!decision)
|
|
2756
|
+
return {
|
|
2757
|
+
sourcePointer,
|
|
2758
|
+
disposition: "pending_review",
|
|
2759
|
+
legacy,
|
|
2760
|
+
reasons: [
|
|
2761
|
+
"No explicit output contract, methodology version or observation association supplied."
|
|
2762
|
+
]
|
|
2763
|
+
};
|
|
2764
|
+
const reasons = [];
|
|
2765
|
+
if (decision.outputContractVersion !== "2.0.0" || SCORING_SCHEMA_VERSION !== "2.0.0")
|
|
2766
|
+
reasons.push(
|
|
2767
|
+
"Unsupported historical scoring output contract; retained without conversion."
|
|
2768
|
+
);
|
|
2769
|
+
const validation = reasons.length ? void 0 : decision.resultKind === "compellingness" ? ScoreResultSchema.safeParse(data) : CompletenessResultSchema.safeParse(data);
|
|
2770
|
+
if (validation && !validation.success)
|
|
2771
|
+
reasons.push(
|
|
2772
|
+
...validation.error.issues.map(
|
|
2773
|
+
(i) => `${i.path.join(".")}: ${i.message}`
|
|
2774
|
+
)
|
|
2775
|
+
);
|
|
2776
|
+
if (decision.resultKind === "compellingness" && data && typeof data === "object" && !Array.isArray(data) && data.scoringVersion !== decision.methodologyVersion)
|
|
2777
|
+
reasons.push(
|
|
2778
|
+
"Declared methodology version does not match the stored scoringVersion"
|
|
2779
|
+
);
|
|
2780
|
+
if (reasons.length)
|
|
2781
|
+
return {
|
|
2782
|
+
sourcePointer,
|
|
2783
|
+
disposition: "quarantined",
|
|
2784
|
+
legacy,
|
|
2785
|
+
decision,
|
|
2786
|
+
reasons
|
|
2787
|
+
};
|
|
2788
|
+
const observationIdentity = `legacy-${hash5(
|
|
2789
|
+
JSON.stringify([plan.namespace, decision.sourceId])
|
|
2790
|
+
)}`;
|
|
2791
|
+
return {
|
|
2792
|
+
sourcePointer,
|
|
2793
|
+
disposition: "preserved_historical",
|
|
2794
|
+
legacy,
|
|
2795
|
+
decision,
|
|
2796
|
+
reasons,
|
|
2797
|
+
historicalOutput: {
|
|
2798
|
+
kind: "legacy_score_history",
|
|
2799
|
+
schemaVersion: "0.1.0",
|
|
2800
|
+
id: `legacy-score-${hash5(
|
|
2801
|
+
JSON.stringify([
|
|
2802
|
+
plan.namespace,
|
|
2803
|
+
sourceDigest,
|
|
2804
|
+
sourcePointer,
|
|
2805
|
+
reviewDigest
|
|
2806
|
+
])
|
|
2807
|
+
)}`,
|
|
2808
|
+
association: {
|
|
2809
|
+
sourceId: decision.sourceId,
|
|
2810
|
+
observationIdentity,
|
|
2811
|
+
verification: "declared_not_verified"
|
|
2812
|
+
},
|
|
2813
|
+
resultKind: decision.resultKind,
|
|
2814
|
+
outputContract: {
|
|
2815
|
+
package: "@disclosureos/scoring",
|
|
2816
|
+
version: decision.outputContractVersion
|
|
2817
|
+
},
|
|
2818
|
+
methodologyVersion: decision.methodologyVersion,
|
|
2819
|
+
// Keep original values, including additional fields. Never substitute stripping parser output.
|
|
2820
|
+
value: data,
|
|
2821
|
+
validation: "historical_output_shape_only",
|
|
2822
|
+
calculation: "not_recomputed",
|
|
2823
|
+
scientificValidity: "not_checked",
|
|
2824
|
+
comparableToV2: false,
|
|
2825
|
+
rankingEligibility: "excluded"
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
});
|
|
2829
|
+
return {
|
|
2830
|
+
kind: "legacy_score_preservation_report",
|
|
2831
|
+
schemaVersion: "0.1.0",
|
|
2832
|
+
policy: "legacy-score-preservation:0.1.0",
|
|
2833
|
+
namespace: plan.namespace,
|
|
2834
|
+
source: archive(source),
|
|
2835
|
+
review: archive(planBytes),
|
|
2836
|
+
reviewedBy: plan.reviewedBy,
|
|
2837
|
+
reviewedAt: plan.reviewedAt,
|
|
2838
|
+
reviewerIdentity: "not_authenticated",
|
|
2839
|
+
records,
|
|
2840
|
+
counts: {
|
|
2841
|
+
input: rows.length,
|
|
2842
|
+
preservedHistorical: records.filter(
|
|
2843
|
+
(r) => r.disposition === "preserved_historical"
|
|
2844
|
+
).length,
|
|
2845
|
+
pendingReview: records.filter((r) => r.disposition === "pending_review").length,
|
|
2846
|
+
quarantined: records.filter((r) => r.disposition === "quarantined").length,
|
|
2847
|
+
v2Evaluations: 0,
|
|
2848
|
+
indexImported: 0
|
|
2849
|
+
},
|
|
2850
|
+
rankingPolicy: "exclude_legacy_scores_from_v2_rankings"
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
function legacyScores(args2) {
|
|
2854
|
+
const usage2 = "migrate legacy-scores <scores.json> <preservation-plan.json> [--json]";
|
|
2855
|
+
if (args2.flags["help"]) {
|
|
2856
|
+
console.log(usage2);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
try {
|
|
2860
|
+
if (args2.positional.length !== 2 || Object.keys(args2.flags).some((k) => k !== "json"))
|
|
2861
|
+
throw new Error(usage2);
|
|
2862
|
+
const report = preserveLegacyScores(
|
|
2863
|
+
readLocal(args2.positional[0], 8 * 1024 * 1024),
|
|
2864
|
+
readLocal(args2.positional[1], 2 * 1024 * 1024)
|
|
2865
|
+
);
|
|
2866
|
+
if (args2.flags["json"]) console.log(JSON.stringify(report, null, 2));
|
|
2867
|
+
else
|
|
2868
|
+
console.log(
|
|
2869
|
+
`Legacy scores: ${report.counts.preservedHistorical} historical outputs preserved, ${report.counts.pendingReview} pending review, ${report.counts.quarantined} quarantined, 0 v2 evaluations.
|
|
2870
|
+
Historical values remain excluded from v2 rankings. Use --json for private retained data and declared associations.`
|
|
2871
|
+
);
|
|
2872
|
+
if (report.counts.quarantined || report.counts.pendingReview)
|
|
2873
|
+
process.exitCode = 1;
|
|
2874
|
+
} catch (error2) {
|
|
2875
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2876
|
+
if (args2.flags["json"])
|
|
2877
|
+
console.log(
|
|
2878
|
+
JSON.stringify({
|
|
2879
|
+
success: false,
|
|
2880
|
+
stage: "legacy_score_input",
|
|
2881
|
+
error: message
|
|
2882
|
+
})
|
|
2883
|
+
);
|
|
2884
|
+
else console.error(message);
|
|
2885
|
+
process.exitCode = 2;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/commands/migration-read-path.ts
|
|
2890
|
+
import { createHash as createHash14, randomUUID as randomUUID3 } from "crypto";
|
|
2891
|
+
import {
|
|
2892
|
+
mkdirSync as mkdirSync6,
|
|
2893
|
+
lstatSync as lstatSync9,
|
|
2894
|
+
readdirSync as readdirSync5,
|
|
2895
|
+
openSync as openSync3,
|
|
2896
|
+
writeFileSync as writeFileSync7,
|
|
2897
|
+
fsyncSync as fsyncSync2,
|
|
2898
|
+
closeSync as closeSync3,
|
|
2899
|
+
linkSync as linkSync2,
|
|
2900
|
+
unlinkSync as unlinkSync2
|
|
2901
|
+
} from "fs";
|
|
2902
|
+
import { resolve as resolve11, join as join9, isAbsolute } from "path";
|
|
2903
|
+
|
|
2904
|
+
// src/commands/migration-apply.ts
|
|
2905
|
+
import { createHash as createHash13, randomUUID as randomUUID2 } from "crypto";
|
|
2906
|
+
import {
|
|
2907
|
+
mkdirSync as mkdirSync5,
|
|
2908
|
+
lstatSync as lstatSync8,
|
|
2909
|
+
readdirSync as readdirSync4,
|
|
2910
|
+
openSync as openSync2,
|
|
2911
|
+
writeFileSync as writeFileSync6,
|
|
2912
|
+
fsyncSync,
|
|
2913
|
+
closeSync as closeSync2,
|
|
2914
|
+
linkSync,
|
|
2915
|
+
unlinkSync
|
|
2916
|
+
} from "fs";
|
|
2917
|
+
import { resolve as resolve10, join as join8 } from "path";
|
|
2918
|
+
|
|
2919
|
+
// src/commands/migration-ledger.ts
|
|
2920
|
+
import { createHash as createHash11 } from "crypto";
|
|
2921
|
+
import {
|
|
2922
|
+
mkdirSync as mkdirSync4,
|
|
2923
|
+
writeFileSync as writeFileSync5,
|
|
2924
|
+
rmSync as rmSync4,
|
|
2925
|
+
lstatSync as lstatSync7,
|
|
2926
|
+
readdirSync as readdirSync3
|
|
2927
|
+
} from "fs";
|
|
2928
|
+
import { resolve as resolve8, join as join7 } from "path";
|
|
2929
|
+
|
|
2930
|
+
// src/commands/migration-export.ts
|
|
2931
|
+
import { createHash as createHash10 } from "crypto";
|
|
2932
|
+
import {
|
|
2933
|
+
mkdirSync as mkdirSync3,
|
|
2934
|
+
writeFileSync as writeFileSync4,
|
|
2935
|
+
rmSync as rmSync3,
|
|
2936
|
+
lstatSync as lstatSync6,
|
|
2937
|
+
readdirSync as readdirSync2
|
|
2938
|
+
} from "fs";
|
|
2939
|
+
import { resolve as resolve7, join as join6 } from "path";
|
|
2940
|
+
var POLICY2 = "legacy-migration-export:0.1.0";
|
|
2941
|
+
var TOTAL_LIMIT2 = 256 * 1024 * 1024;
|
|
2942
|
+
var json3 = (value) => Buffer.from(JSON.stringify(value, null, 2) + "\n");
|
|
2943
|
+
var pin2 = (bytes2) => ({
|
|
2944
|
+
sha256: createHash10("sha256").update(bytes2).digest("hex"),
|
|
2945
|
+
byteLength: bytes2.byteLength
|
|
2946
|
+
});
|
|
2947
|
+
function assembleMigrationBundle(source, review) {
|
|
2948
|
+
const report = buildReviewedMigration(source, review);
|
|
2949
|
+
const files = /* @__PURE__ */ new Map([
|
|
2950
|
+
["source.json", source],
|
|
2951
|
+
["review.json", review]
|
|
2952
|
+
]);
|
|
2953
|
+
let total = source.byteLength + review.byteLength;
|
|
2954
|
+
const add = (path, bytes2) => {
|
|
2955
|
+
total += bytes2.byteLength;
|
|
2956
|
+
if (total > TOTAL_LIMIT2)
|
|
2957
|
+
throw new Error("Migration export exceeds 256 MiB budget");
|
|
2958
|
+
files.set(path, bytes2);
|
|
2959
|
+
};
|
|
2960
|
+
add("report.json", json3(report));
|
|
2961
|
+
const rows = report.records.map((row) => {
|
|
2962
|
+
if ("candidate" in row && row.candidate && row.review.status === "applied") {
|
|
2963
|
+
const path = `candidates/${row.candidate.observation.id}.json`;
|
|
2964
|
+
add(path, json3(row.candidate));
|
|
2965
|
+
return {
|
|
2966
|
+
sourcePointer: row.sourcePointer,
|
|
2967
|
+
sourceId: row.sourceId,
|
|
2968
|
+
disposition: "exported",
|
|
2969
|
+
path
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
return {
|
|
2973
|
+
sourcePointer: row.sourcePointer,
|
|
2974
|
+
sourceId: row.sourceId,
|
|
2975
|
+
disposition: row.decision === "quarantined" ? "quarantined" : "pending_review"
|
|
2976
|
+
};
|
|
2977
|
+
});
|
|
2978
|
+
const count = (disposition) => rows.filter((row) => row.disposition === disposition).length;
|
|
2979
|
+
const manifest2 = {
|
|
2980
|
+
kind: "legacy_migration_export",
|
|
2981
|
+
schemaVersion: "0.1.0",
|
|
2982
|
+
policy: POLICY2,
|
|
2983
|
+
namespace: report.namespace,
|
|
2984
|
+
counts: {
|
|
2985
|
+
input: rows.length,
|
|
2986
|
+
exported: count("exported"),
|
|
2987
|
+
pendingReview: count("pending_review"),
|
|
2988
|
+
quarantined: count("quarantined"),
|
|
2989
|
+
migrated: 0
|
|
2990
|
+
},
|
|
2991
|
+
rows,
|
|
2992
|
+
files: [...files].map(([path, bytes2]) => ({ path, ...pin2(bytes2) })),
|
|
2993
|
+
reviewerIdentity: "not_authenticated",
|
|
2994
|
+
scientificValidity: "not_checked"
|
|
2995
|
+
};
|
|
2996
|
+
add("manifest.json", json3(manifest2));
|
|
2997
|
+
return { files, manifest: manifest2 };
|
|
2998
|
+
}
|
|
2999
|
+
function privateDirectory(path) {
|
|
3000
|
+
const stat2 = lstatSync6(path);
|
|
3001
|
+
if (!stat2.isDirectory() || (stat2.mode & 63) !== 0)
|
|
3002
|
+
throw new Error("Expected a private real directory (0700)");
|
|
3003
|
+
}
|
|
3004
|
+
function compare(destination, bundle) {
|
|
3005
|
+
privateDirectory(destination);
|
|
3006
|
+
privateDirectory(join6(destination, "candidates"));
|
|
3007
|
+
const rootNames = [
|
|
3008
|
+
"candidates",
|
|
3009
|
+
"manifest.json",
|
|
3010
|
+
"report.json",
|
|
3011
|
+
"review.json",
|
|
3012
|
+
"source.json"
|
|
3013
|
+
];
|
|
3014
|
+
const candidateNames = [...bundle.files.keys()].filter((path) => path.startsWith("candidates/")).map((path) => path.slice(11));
|
|
3015
|
+
if (JSON.stringify(readdirSync2(destination).sort()) !== JSON.stringify(rootNames.sort()) || JSON.stringify(readdirSync2(join6(destination, "candidates")).sort()) !== JSON.stringify(candidateNames.sort())) {
|
|
3016
|
+
throw new Error("Bundle conflict: unexpected or missing files");
|
|
3017
|
+
}
|
|
3018
|
+
for (const [path, expected] of bundle.files) {
|
|
3019
|
+
const file = join6(destination, path);
|
|
3020
|
+
const stat2 = lstatSync6(file);
|
|
3021
|
+
if (!stat2.isFile() || (stat2.mode & 63) !== 0)
|
|
3022
|
+
throw new Error(`Bundle conflict: nonprivate or nonregular file ${path}`);
|
|
3023
|
+
const actual = readLocal(file, expected.byteLength);
|
|
3024
|
+
if (!Buffer.from(actual).equals(Buffer.from(expected)))
|
|
3025
|
+
throw new Error(`Bundle conflict: content differs at ${path}`);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
function exportMigration(args2) {
|
|
3029
|
+
if (args2.flags["help"]) {
|
|
3030
|
+
console.log(
|
|
3031
|
+
"disclosureos migrate export <legacy.json> <review.json> --out <bundle> [--json]\ndisclosureos migrate verify <bundle> [--json]\nPrivate review artifacts only; no database imports."
|
|
3032
|
+
);
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
let created;
|
|
3036
|
+
try {
|
|
3037
|
+
const verify = args2.subcommand === "verify";
|
|
3038
|
+
const output = args2.flags["out"];
|
|
3039
|
+
if (verify ? args2.positional.length !== 1 || Object.keys(args2.flags).some((key) => key !== "json") : args2.positional.length !== 2 || typeof output !== "string" || Object.keys(args2.flags).some((key) => !["out", "json"].includes(key))) {
|
|
3040
|
+
throw new Error(
|
|
3041
|
+
"Usage: migrate export <legacy.json> <review.json> --out <bundle> [--json], or migrate verify <bundle> [--json]"
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
const destination = resolve7(
|
|
3045
|
+
verify ? args2.positional[0] : output
|
|
3046
|
+
);
|
|
3047
|
+
if (verify) privateDirectory(destination);
|
|
3048
|
+
const source = readLocal(
|
|
3049
|
+
verify ? join6(destination, "source.json") : args2.positional[0],
|
|
3050
|
+
8 * 1024 * 1024
|
|
3051
|
+
);
|
|
3052
|
+
const review = readLocal(
|
|
3053
|
+
verify ? join6(destination, "review.json") : args2.positional[1],
|
|
3054
|
+
2 * 1024 * 1024
|
|
3055
|
+
);
|
|
3056
|
+
const bundle = assembleMigrationBundle(source, review);
|
|
3057
|
+
let action = "verified";
|
|
3058
|
+
if (!verify) {
|
|
3059
|
+
try {
|
|
3060
|
+
mkdirSync3(destination, { mode: 448 });
|
|
3061
|
+
created = destination;
|
|
3062
|
+
} catch (error2) {
|
|
3063
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
3064
|
+
}
|
|
3065
|
+
if (created) {
|
|
3066
|
+
mkdirSync3(join6(destination, "candidates"), { mode: 448 });
|
|
3067
|
+
for (const [path, bytes2] of bundle.files)
|
|
3068
|
+
writeFileSync4(join6(destination, path), bytes2, {
|
|
3069
|
+
mode: 384,
|
|
3070
|
+
flag: "wx"
|
|
3071
|
+
});
|
|
3072
|
+
action = "created";
|
|
3073
|
+
} else action = "reused";
|
|
3074
|
+
}
|
|
3075
|
+
compare(destination, bundle);
|
|
3076
|
+
created = void 0;
|
|
3077
|
+
const result = {
|
|
3078
|
+
success: true,
|
|
3079
|
+
action,
|
|
3080
|
+
directory: destination,
|
|
3081
|
+
policy: POLICY2,
|
|
3082
|
+
counts: bundle.manifest.counts,
|
|
3083
|
+
manifest: pin2(bundle.files.get("manifest.json")),
|
|
3084
|
+
verification: "current_code_replay",
|
|
3085
|
+
migrationStatus: "not_imported",
|
|
3086
|
+
reviewerIdentity: "not_authenticated",
|
|
3087
|
+
scientificValidity: "not_checked"
|
|
3088
|
+
};
|
|
3089
|
+
if (args2.flags["json"]) console.log(JSON.stringify(result, null, 2));
|
|
3090
|
+
else
|
|
3091
|
+
console.log(
|
|
3092
|
+
`Migration bundle ${action}: ${result.counts.exported} exported, ${result.counts.pendingReview} pending review, ${result.counts.quarantined} quarantined, 0 migrated.
|
|
3093
|
+
${destination}`
|
|
3094
|
+
);
|
|
3095
|
+
if (result.counts.quarantined || result.counts.pendingReview)
|
|
3096
|
+
process.exitCode = 1;
|
|
3097
|
+
} catch (error2) {
|
|
3098
|
+
if (created) rmSync3(created, { recursive: true, force: true });
|
|
3099
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3100
|
+
if (args2.flags["json"])
|
|
3101
|
+
console.log(
|
|
3102
|
+
JSON.stringify({
|
|
3103
|
+
success: false,
|
|
3104
|
+
stage: "migration_export",
|
|
3105
|
+
error: message
|
|
3106
|
+
})
|
|
3107
|
+
);
|
|
3108
|
+
else console.error(message);
|
|
3109
|
+
process.exitCode = 2;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
function verifiedMigrationBundle(destination) {
|
|
3113
|
+
privateDirectory(destination);
|
|
3114
|
+
const bundle = assembleMigrationBundle(
|
|
3115
|
+
readLocal(join6(destination, "source.json"), 8 * 1024 * 1024),
|
|
3116
|
+
readLocal(join6(destination, "review.json"), 2 * 1024 * 1024)
|
|
3117
|
+
);
|
|
3118
|
+
compare(destination, bundle);
|
|
3119
|
+
return bundle;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
// src/commands/migration-ledger.ts
|
|
3123
|
+
var POLICY3 = "legacy-migration-ledger:0.1.0";
|
|
3124
|
+
var pin3 = (bytes2) => ({
|
|
3125
|
+
sha256: createHash11("sha256").update(bytes2).digest("hex"),
|
|
3126
|
+
byteLength: bytes2.byteLength
|
|
3127
|
+
});
|
|
3128
|
+
var json4 = (value) => Buffer.from(JSON.stringify(value, null, 2) + "\n");
|
|
3129
|
+
var privateDir = (path) => {
|
|
3130
|
+
const s = lstatSync7(path);
|
|
3131
|
+
if (!s.isDirectory() || s.mode & 63)
|
|
3132
|
+
throw new Error("Expected a private real ledger directory");
|
|
3133
|
+
};
|
|
3134
|
+
function planLedger(bundles) {
|
|
3135
|
+
const unique = new Map(
|
|
3136
|
+
bundles.map((bundle) => [
|
|
3137
|
+
pin3(bundle.files.get("manifest.json")).sha256,
|
|
3138
|
+
bundle
|
|
3139
|
+
])
|
|
3140
|
+
);
|
|
3141
|
+
const files = /* @__PURE__ */ new Map();
|
|
3142
|
+
const dirs = /* @__PURE__ */ new Set(["", "inputs"]);
|
|
3143
|
+
let total = 0;
|
|
3144
|
+
const add = (path, bytes2) => {
|
|
3145
|
+
total += bytes2.byteLength;
|
|
3146
|
+
if (total > 256 * 1024 * 1024 || files.size >= 2e4)
|
|
3147
|
+
throw new Error("Ledger exceeds 256 MiB or 20000-file budget");
|
|
3148
|
+
files.set(path, bytes2);
|
|
3149
|
+
};
|
|
3150
|
+
const identities = /* @__PURE__ */ new Map();
|
|
3151
|
+
const receipts = [...unique].sort(([a], [b]) => a.localeCompare(b)).map(([bundleId, bundle]) => {
|
|
3152
|
+
const root = `inputs/${bundleId}`;
|
|
3153
|
+
dirs.add(root);
|
|
3154
|
+
dirs.add(`${root}/candidates`);
|
|
3155
|
+
for (const [path, bytes2] of bundle.files) add(`${root}/${path}`, bytes2);
|
|
3156
|
+
const rows2 = bundle.manifest.rows.map((row) => {
|
|
3157
|
+
if (row.disposition !== "exported" || !row.path)
|
|
3158
|
+
return { ...row, status: row.disposition };
|
|
3159
|
+
const path = `${root}/${row.path}`;
|
|
3160
|
+
const revision = pin3(bundle.files.get(row.path)).sha256;
|
|
3161
|
+
const identity = row.path.slice("candidates/".length, -".json".length);
|
|
3162
|
+
const revisions = identities.get(identity) ?? /* @__PURE__ */ new Map();
|
|
3163
|
+
const occurrences = revisions.get(revision) ?? [];
|
|
3164
|
+
occurrences.push({ bundleId, sourcePointer: row.sourcePointer, path });
|
|
3165
|
+
revisions.set(revision, occurrences);
|
|
3166
|
+
identities.set(identity, revisions);
|
|
3167
|
+
return { ...row, identity, revision, status: "staged" };
|
|
3168
|
+
});
|
|
3169
|
+
return {
|
|
3170
|
+
bundleId,
|
|
3171
|
+
namespace: bundle.manifest.namespace,
|
|
3172
|
+
manifest: pin3(bundle.files.get("manifest.json")),
|
|
3173
|
+
rows: rows2
|
|
3174
|
+
};
|
|
3175
|
+
});
|
|
3176
|
+
const inventory = [...identities].sort(([a], [b]) => a.localeCompare(b)).map(([identity, revisions]) => ({
|
|
3177
|
+
identity,
|
|
3178
|
+
status: revisions.size === 1 ? "ready_for_import_review" : "conflict",
|
|
3179
|
+
revisions: [...revisions].sort(([a], [b]) => a.localeCompare(b)).map(([sha2562, occurrences]) => ({ sha256: sha2562, occurrences }))
|
|
3180
|
+
}));
|
|
3181
|
+
const conflicts = new Set(
|
|
3182
|
+
inventory.filter((item) => item.status === "conflict").map((item) => item.identity)
|
|
3183
|
+
);
|
|
3184
|
+
const finalized = receipts.map((receipt) => ({
|
|
3185
|
+
...receipt,
|
|
3186
|
+
rows: receipt.rows.map((row) => ({
|
|
3187
|
+
...row,
|
|
3188
|
+
status: "identity" in row && conflicts.has(row.identity) ? "conflict" : row.status
|
|
3189
|
+
}))
|
|
3190
|
+
}));
|
|
3191
|
+
const rows = finalized.flatMap((receipt) => receipt.rows);
|
|
3192
|
+
const count = (status2) => rows.filter((row) => row.status === status2).length;
|
|
3193
|
+
const ledger = {
|
|
3194
|
+
kind: "legacy_migration_ledger",
|
|
3195
|
+
schemaVersion: "0.1.0",
|
|
3196
|
+
policy: POLICY3,
|
|
3197
|
+
counts: {
|
|
3198
|
+
bundles: unique.size,
|
|
3199
|
+
input: rows.length,
|
|
3200
|
+
stagedRows: count("staged"),
|
|
3201
|
+
conflictRows: count("conflict"),
|
|
3202
|
+
pendingReview: count("pending_review"),
|
|
3203
|
+
quarantined: count("quarantined"),
|
|
3204
|
+
identities: inventory.length,
|
|
3205
|
+
conflictingIdentities: conflicts.size,
|
|
3206
|
+
migrated: 0
|
|
3207
|
+
},
|
|
3208
|
+
receipts: finalized,
|
|
3209
|
+
identities: inventory,
|
|
3210
|
+
files: [...files].map(([path, bytes2]) => ({ path, ...pin3(bytes2) })),
|
|
3211
|
+
verification: "current_code_replay",
|
|
3212
|
+
reviewerIdentity: "not_authenticated",
|
|
3213
|
+
applicationStatus: "not_imported"
|
|
3214
|
+
};
|
|
3215
|
+
add("ledger.json", json4(ledger));
|
|
3216
|
+
return { files, dirs, ledger };
|
|
3217
|
+
}
|
|
3218
|
+
function compareLedger(root, plan) {
|
|
3219
|
+
for (const dir of plan.dirs) {
|
|
3220
|
+
const path = join7(root, dir);
|
|
3221
|
+
privateDir(path);
|
|
3222
|
+
const expected = /* @__PURE__ */ new Set();
|
|
3223
|
+
const prefix = dir ? `${dir}/` : "";
|
|
3224
|
+
for (const name2 of [...plan.dirs, ...plan.files.keys()])
|
|
3225
|
+
if (name2.startsWith(prefix) && name2 !== dir)
|
|
3226
|
+
expected.add(name2.slice(prefix.length).split("/")[0]);
|
|
3227
|
+
if (JSON.stringify(readdirSync3(path).sort()) !== JSON.stringify([...expected].sort()))
|
|
3228
|
+
throw new Error("Ledger conflict: missing or unexpected entries");
|
|
3229
|
+
}
|
|
3230
|
+
for (const [path, bytes2] of plan.files) {
|
|
3231
|
+
const file = join7(root, path), stat2 = lstatSync7(file);
|
|
3232
|
+
if (!stat2.isFile() || stat2.mode & 63)
|
|
3233
|
+
throw new Error("Ledger conflict: nonprivate or nonregular artifact");
|
|
3234
|
+
if (!Buffer.from(readLocal(file, bytes2.byteLength)).equals(Buffer.from(bytes2)))
|
|
3235
|
+
throw new Error(`Ledger conflict: changed artifact ${path}`);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
function migrationLedger(args2) {
|
|
3239
|
+
if (args2.flags["help"]) {
|
|
3240
|
+
console.log(
|
|
3241
|
+
"migrate ledger <bundle>... --out <ledger> [--json]\nmigrate ledger-verify <ledger> [--json]\nImmutable local staging receipts; no application imports."
|
|
3242
|
+
);
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
let created;
|
|
3246
|
+
try {
|
|
3247
|
+
const verify = args2.subcommand === "ledger-verify", output = args2.flags["out"];
|
|
3248
|
+
if (verify ? args2.positional.length !== 1 || Object.keys(args2.flags).some((key) => key !== "json") : !args2.positional.length || args2.positional.length > 16 || typeof output !== "string" || Object.keys(args2.flags).some((key) => !["out", "json"].includes(key)))
|
|
3249
|
+
throw new Error(
|
|
3250
|
+
"Usage: migrate ledger <1-16 bundles> --out <ledger>, or migrate ledger-verify <ledger>"
|
|
3251
|
+
);
|
|
3252
|
+
const root = resolve8(verify ? args2.positional[0] : output);
|
|
3253
|
+
let paths;
|
|
3254
|
+
if (verify) {
|
|
3255
|
+
privateDir(root);
|
|
3256
|
+
privateDir(join7(root, "inputs"));
|
|
3257
|
+
const ids = readdirSync3(join7(root, "inputs")).sort();
|
|
3258
|
+
if (!ids.length || ids.length > 16 || ids.some((id) => !/^[a-f0-9]{64}$/.test(id)))
|
|
3259
|
+
throw new Error("Invalid ledger input inventory");
|
|
3260
|
+
paths = ids.map((id) => join7(root, "inputs", id));
|
|
3261
|
+
} else paths = args2.positional.map((path) => resolve8(path));
|
|
3262
|
+
let bytes2 = 0, entries = 0;
|
|
3263
|
+
const bundles = paths.map((path) => {
|
|
3264
|
+
const bundle = verifiedMigrationBundle(path);
|
|
3265
|
+
for (const data of bundle.files.values()) bytes2 += data.byteLength;
|
|
3266
|
+
entries += bundle.files.size;
|
|
3267
|
+
if (bytes2 > 256 * 1024 * 1024 || entries >= 2e4)
|
|
3268
|
+
throw new Error("Ledger input budget exceeded");
|
|
3269
|
+
return bundle;
|
|
3270
|
+
});
|
|
3271
|
+
const plan = planLedger(bundles);
|
|
3272
|
+
let action = "verified";
|
|
3273
|
+
if (!verify) {
|
|
3274
|
+
try {
|
|
3275
|
+
mkdirSync4(root, { mode: 448 });
|
|
3276
|
+
created = root;
|
|
3277
|
+
} catch (error2) {
|
|
3278
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
3279
|
+
}
|
|
3280
|
+
if (created) {
|
|
3281
|
+
for (const dir of plan.dirs)
|
|
3282
|
+
if (dir) mkdirSync4(join7(root, dir), { mode: 448 });
|
|
3283
|
+
for (const [path, data] of plan.files)
|
|
3284
|
+
writeFileSync5(join7(root, path), data, { mode: 384, flag: "wx" });
|
|
3285
|
+
action = "created";
|
|
3286
|
+
} else action = "reused";
|
|
3287
|
+
}
|
|
3288
|
+
compareLedger(root, plan);
|
|
3289
|
+
created = void 0;
|
|
3290
|
+
const result = {
|
|
3291
|
+
success: true,
|
|
3292
|
+
action,
|
|
3293
|
+
directory: root,
|
|
3294
|
+
ledger: pin3(plan.files.get("ledger.json")),
|
|
3295
|
+
counts: plan.ledger.counts,
|
|
3296
|
+
applicationStatus: "not_imported",
|
|
3297
|
+
verification: "current_code_replay"
|
|
3298
|
+
};
|
|
3299
|
+
if (args2.flags["json"]) console.log(JSON.stringify(result, null, 2));
|
|
3300
|
+
else
|
|
3301
|
+
console.log(
|
|
3302
|
+
`Migration ledger ${action}: ${result.counts.stagedRows} staged, ${result.counts.conflictRows} conflicting, ${result.counts.pendingReview} pending, ${result.counts.quarantined} quarantined, 0 migrated.
|
|
3303
|
+
${root}`
|
|
3304
|
+
);
|
|
3305
|
+
if (result.counts.conflictRows || result.counts.pendingReview || result.counts.quarantined)
|
|
3306
|
+
process.exitCode = 1;
|
|
3307
|
+
} catch (error2) {
|
|
3308
|
+
if (created) rmSync4(created, { recursive: true, force: true });
|
|
3309
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3310
|
+
if (args2.flags["json"])
|
|
3311
|
+
console.log(
|
|
3312
|
+
JSON.stringify({
|
|
3313
|
+
success: false,
|
|
3314
|
+
stage: "migration_ledger",
|
|
3315
|
+
error: message
|
|
3316
|
+
})
|
|
3317
|
+
);
|
|
3318
|
+
else console.error(message);
|
|
3319
|
+
process.exitCode = 2;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
function verifiedMigrationLedger(root) {
|
|
3323
|
+
privateDir(root);
|
|
3324
|
+
privateDir(join7(root, "inputs"));
|
|
3325
|
+
const ids = readdirSync3(join7(root, "inputs")).sort();
|
|
3326
|
+
if (!ids.length || ids.length > 16 || ids.some((id) => !/^[a-f0-9]{64}$/.test(id)))
|
|
3327
|
+
throw new Error("Invalid ledger input inventory");
|
|
3328
|
+
let bytes2 = 0, entries = 0;
|
|
3329
|
+
const bundles = ids.map((id) => {
|
|
3330
|
+
const bundle = verifiedMigrationBundle(join7(root, "inputs", id));
|
|
3331
|
+
for (const data of bundle.files.values()) bytes2 += data.byteLength;
|
|
3332
|
+
entries += bundle.files.size;
|
|
3333
|
+
if (bytes2 > 256 * 1024 * 1024 || entries >= 2e4)
|
|
3334
|
+
throw new Error("Ledger input budget exceeded");
|
|
3335
|
+
return bundle;
|
|
3336
|
+
});
|
|
3337
|
+
const plan = planLedger(bundles);
|
|
3338
|
+
compareLedger(root, plan);
|
|
3339
|
+
return plan;
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
// src/commands/migration-resolution.ts
|
|
3343
|
+
import { createHash as createHash12 } from "crypto";
|
|
3344
|
+
import { resolve as resolve9 } from "path";
|
|
3345
|
+
import {
|
|
3346
|
+
MigrationResolutionSchema
|
|
3347
|
+
} from "@disclosureos/schema/experimental/v2";
|
|
3348
|
+
var pin4 = (bytes2) => ({
|
|
3349
|
+
sha256: createHash12("sha256").update(bytes2).digest("hex"),
|
|
3350
|
+
byteLength: bytes2.byteLength
|
|
3351
|
+
});
|
|
3352
|
+
function resolveMigration(args2) {
|
|
3353
|
+
if (args2.flags["help"]) {
|
|
3354
|
+
console.log(
|
|
3355
|
+
"migrate resolve <ledger> <decisions.json> [--json]\nExplicit pinned selection/defer decisions only; no application imports."
|
|
3356
|
+
);
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
try {
|
|
3360
|
+
if (args2.positional.length !== 2 || Object.keys(args2.flags).some((key) => key !== "json"))
|
|
3361
|
+
throw new Error(
|
|
3362
|
+
"Usage: migrate resolve <ledger> <decisions.json> [--json]"
|
|
3363
|
+
);
|
|
3364
|
+
const bytes2 = readLocal(args2.positional[1], 2 * 1024 * 1024);
|
|
3365
|
+
const snapshot = verifiedMigrationLedger(resolve9(args2.positional[0]));
|
|
3366
|
+
const report = buildMigrationResolution(snapshot, bytes2);
|
|
3367
|
+
if (args2.flags["json"]) console.log(JSON.stringify(report, null, 2));
|
|
3368
|
+
else {
|
|
3369
|
+
console.log(
|
|
3370
|
+
`Migration resolution: ${report.selected.length} selected candidates, ${report.counts.deferredRows} deferred rows, ${report.counts.unresolvedRows} unresolved rows, ${report.counts.pendingReview} pending, ${report.counts.quarantined} quarantined, 0 migrated.`
|
|
3371
|
+
);
|
|
3372
|
+
for (const identity of report.identities)
|
|
3373
|
+
console.log(
|
|
3374
|
+
JSON.stringify({
|
|
3375
|
+
identity: identity.identity,
|
|
3376
|
+
status: identity.status,
|
|
3377
|
+
decision: identity.decision
|
|
3378
|
+
})
|
|
3379
|
+
);
|
|
3380
|
+
console.log(
|
|
3381
|
+
"Selections are application planning choices, not scientific findings. Use --json for pinned decisions and complete row accounting."
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
if (report.counts.deferredRows || report.counts.unresolvedRows || report.counts.pendingReview || report.counts.quarantined)
|
|
3385
|
+
process.exitCode = 1;
|
|
3386
|
+
} catch (error2) {
|
|
3387
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3388
|
+
if (args2.flags["json"])
|
|
3389
|
+
console.log(
|
|
3390
|
+
JSON.stringify({
|
|
3391
|
+
success: false,
|
|
3392
|
+
stage: "migration_resolution",
|
|
3393
|
+
error: message
|
|
3394
|
+
})
|
|
3395
|
+
);
|
|
3396
|
+
else console.error(message);
|
|
3397
|
+
process.exitCode = 2;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
function buildMigrationResolution(snapshot, bytes2) {
|
|
3401
|
+
const parsed = MigrationResolutionSchema.safeParse(reviewedJson(bytes2));
|
|
3402
|
+
if (!parsed.success)
|
|
3403
|
+
throw new Error(`Invalid resolution plan: ${parsed.error.message}`);
|
|
3404
|
+
const plan = parsed.data;
|
|
3405
|
+
const ledgerPin = pin4(snapshot.files.get("ledger.json"));
|
|
3406
|
+
if (plan.ledger.sha256 !== ledgerPin.sha256 || plan.ledger.byteLength !== ledgerPin.byteLength)
|
|
3407
|
+
throw new Error(
|
|
3408
|
+
"Ledger byte pin mismatch; decisions require the exact verified snapshot"
|
|
3409
|
+
);
|
|
3410
|
+
const inventory = new Map(
|
|
3411
|
+
snapshot.ledger.identities.map((identity) => [identity.identity, identity])
|
|
3412
|
+
);
|
|
3413
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
3414
|
+
for (const decision of plan.decisions) {
|
|
3415
|
+
if (decisions.has(decision.identity))
|
|
3416
|
+
throw new Error("Duplicate resolution decision for identity");
|
|
3417
|
+
const identity = inventory.get(decision.identity);
|
|
3418
|
+
if (!identity) throw new Error("Resolution names an unknown identity");
|
|
3419
|
+
if (decision.action === "select" && !identity.revisions.some(
|
|
3420
|
+
(revision) => revision.sha256 === decision.revision
|
|
3421
|
+
))
|
|
3422
|
+
throw new Error(
|
|
3423
|
+
"Selected revision does not belong to the named identity"
|
|
3424
|
+
);
|
|
3425
|
+
decisions.set(decision.identity, decision);
|
|
3426
|
+
}
|
|
3427
|
+
const identities = snapshot.ledger.identities.map((identity) => {
|
|
3428
|
+
const decision = decisions.get(identity.identity);
|
|
3429
|
+
return {
|
|
3430
|
+
...identity,
|
|
3431
|
+
ledgerStatus: identity.status,
|
|
3432
|
+
status: decision?.action === "select" ? "selected" : decision ? "deferred" : "unresolved",
|
|
3433
|
+
...decision ? { decision } : {}
|
|
3434
|
+
};
|
|
3435
|
+
});
|
|
3436
|
+
const selected = identities.flatMap((identity) => {
|
|
3437
|
+
const decision = decisions.get(identity.identity);
|
|
3438
|
+
if (decision?.action !== "select") return [];
|
|
3439
|
+
const revision = identity.revisions.find(
|
|
3440
|
+
(revision2) => revision2.sha256 === decision.revision
|
|
3441
|
+
);
|
|
3442
|
+
const occurrence = revision.occurrences[0];
|
|
3443
|
+
return [
|
|
3444
|
+
{
|
|
3445
|
+
identity: identity.identity,
|
|
3446
|
+
revision: revision.sha256,
|
|
3447
|
+
path: occurrence.path,
|
|
3448
|
+
candidate: pin4(snapshot.files.get(occurrence.path)),
|
|
3449
|
+
occurrences: revision.occurrences
|
|
3450
|
+
}
|
|
3451
|
+
];
|
|
3452
|
+
});
|
|
3453
|
+
const receipts = snapshot.ledger.receipts.map((receipt) => ({
|
|
3454
|
+
...receipt,
|
|
3455
|
+
rows: receipt.rows.map((row) => {
|
|
3456
|
+
if (!("identity" in row)) return { ...row, ledgerStatus: row.status };
|
|
3457
|
+
const decision = decisions.get(row.identity);
|
|
3458
|
+
const status2 = decision?.action === "select" ? decision.revision === row.revision ? "selected" : "not_selected" : decision ? "deferred" : "unresolved";
|
|
3459
|
+
return { ...row, ledgerStatus: row.status, status: status2 };
|
|
3460
|
+
})
|
|
3461
|
+
}));
|
|
3462
|
+
const rows = receipts.flatMap((receipt) => receipt.rows), count = (status2) => rows.filter((row) => row.status === status2).length;
|
|
3463
|
+
return {
|
|
3464
|
+
kind: "legacy_migration_resolution_result",
|
|
3465
|
+
schemaVersion: "0.1.0",
|
|
3466
|
+
policy: "legacy-migration-resolution:0.1.0",
|
|
3467
|
+
ledger: ledgerPin,
|
|
3468
|
+
plan: {
|
|
3469
|
+
...pin4(bytes2),
|
|
3470
|
+
encoding: "base64",
|
|
3471
|
+
bytes: Buffer.from(bytes2).toString("base64")
|
|
3472
|
+
},
|
|
3473
|
+
decidedBy: plan.decidedBy,
|
|
3474
|
+
decidedAt: plan.decidedAt,
|
|
3475
|
+
identities,
|
|
3476
|
+
selected,
|
|
3477
|
+
receipts,
|
|
3478
|
+
counts: {
|
|
3479
|
+
input: rows.length,
|
|
3480
|
+
selectedRows: count("selected"),
|
|
3481
|
+
notSelectedRows: count("not_selected"),
|
|
3482
|
+
deferredRows: count("deferred"),
|
|
3483
|
+
unresolvedRows: count("unresolved"),
|
|
3484
|
+
pendingReview: count("pending_review"),
|
|
3485
|
+
quarantined: count("quarantined"),
|
|
3486
|
+
selectedCandidates: selected.length,
|
|
3487
|
+
migrated: 0
|
|
3488
|
+
},
|
|
3489
|
+
decisionIdentity: "not_authenticated",
|
|
3490
|
+
scientificValidity: "not_checked",
|
|
3491
|
+
applicationStatus: "not_imported",
|
|
3492
|
+
verification: "current_code_replay"
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
// src/commands/migration-apply.ts
|
|
3497
|
+
var POLICY4 = "legacy-migration-application:0.1.0";
|
|
3498
|
+
var LIMIT3 = 256 * 1024 * 1024;
|
|
3499
|
+
var pin5 = (bytes2) => ({
|
|
3500
|
+
sha256: createHash13("sha256").update(bytes2).digest("hex"),
|
|
3501
|
+
byteLength: bytes2.byteLength
|
|
3502
|
+
});
|
|
3503
|
+
var json5 = (value) => Buffer.from(JSON.stringify(value, null, 2) + "\n");
|
|
3504
|
+
var stat = (path) => {
|
|
3505
|
+
try {
|
|
3506
|
+
return lstatSync8(path);
|
|
3507
|
+
} catch (error2) {
|
|
3508
|
+
if (error2.code === "ENOENT") return void 0;
|
|
3509
|
+
throw error2;
|
|
3510
|
+
}
|
|
3511
|
+
};
|
|
3512
|
+
function directory4(path) {
|
|
3513
|
+
const s = lstatSync8(path);
|
|
3514
|
+
if (!s.isDirectory() || s.mode & 63)
|
|
3515
|
+
throw new Error("Expected a private real application directory");
|
|
3516
|
+
}
|
|
3517
|
+
function exact(path, bytes2) {
|
|
3518
|
+
const s = lstatSync8(path);
|
|
3519
|
+
if (!s.isFile() || s.mode & 63)
|
|
3520
|
+
throw new Error("Application artifact must be a private regular file");
|
|
3521
|
+
if (!Buffer.from(readLocal(path, bytes2.byteLength)).equals(Buffer.from(bytes2)))
|
|
3522
|
+
throw new Error(`Application conflict: changed artifact ${path}`);
|
|
3523
|
+
}
|
|
3524
|
+
function prepare(ledger, decisions) {
|
|
3525
|
+
const snapshot = verifiedMigrationLedger(ledger), resolution = buildMigrationResolution(snapshot, decisions), c = resolution.counts;
|
|
3526
|
+
if (c.deferredRows || c.unresolvedRows || c.pendingReview || c.quarantined || !resolution.selected.length)
|
|
3527
|
+
throw new Error(
|
|
3528
|
+
"Local application requires explicit selections for all identities, no pending/quarantined rows and at least one candidate"
|
|
3529
|
+
);
|
|
3530
|
+
const files = /* @__PURE__ */ new Map(), dirs = /* @__PURE__ */ new Set(["", ".pending", "ledger", "candidates"]);
|
|
3531
|
+
let total = 0;
|
|
3532
|
+
const add = (path, bytes2) => {
|
|
3533
|
+
total += bytes2.byteLength;
|
|
3534
|
+
if (total > LIMIT3 || files.size >= 3e4)
|
|
3535
|
+
throw new Error("Application exceeds 256 MiB or 30000-file budget");
|
|
3536
|
+
files.set(path, bytes2);
|
|
3537
|
+
};
|
|
3538
|
+
for (const dir of snapshot.dirs) if (dir) dirs.add(`ledger/${dir}`);
|
|
3539
|
+
for (const [path, bytes2] of snapshot.files) add(`ledger/${path}`, bytes2);
|
|
3540
|
+
add("resolution.json", decisions);
|
|
3541
|
+
add("resolution-report.json", json5(resolution));
|
|
3542
|
+
const candidates = resolution.selected.map((item) => {
|
|
3543
|
+
const path = `candidates/${item.identity}.json`;
|
|
3544
|
+
const bytes2 = snapshot.files.get(item.path);
|
|
3545
|
+
add(path, bytes2);
|
|
3546
|
+
return { ...item, path };
|
|
3547
|
+
});
|
|
3548
|
+
const receipt = {
|
|
3549
|
+
kind: "legacy_migration_application_receipt",
|
|
3550
|
+
schemaVersion: "0.1.0",
|
|
3551
|
+
policy: POLICY4,
|
|
3552
|
+
status: "complete",
|
|
3553
|
+
target: "private_local_candidate_store",
|
|
3554
|
+
ledger: resolution.ledger,
|
|
3555
|
+
resolution: pin5(decisions),
|
|
3556
|
+
counts: {
|
|
3557
|
+
inputRows: c.input,
|
|
3558
|
+
selectedRows: c.selectedRows,
|
|
3559
|
+
retainedHistoricalRows: c.notSelectedRows,
|
|
3560
|
+
appliedCandidates: candidates.length,
|
|
3561
|
+
indexImported: 0
|
|
3562
|
+
},
|
|
3563
|
+
candidates,
|
|
3564
|
+
files: [...files].map(([path, bytes2]) => ({ path, ...pin5(bytes2) })),
|
|
3565
|
+
decisionIdentity: "not_authenticated",
|
|
3566
|
+
scientificValidity: "not_checked",
|
|
3567
|
+
readPath: "not_activated"
|
|
3568
|
+
};
|
|
3569
|
+
add("receipt.json", json5(receipt));
|
|
3570
|
+
const intent = json5({
|
|
3571
|
+
kind: "legacy_migration_application_intent",
|
|
3572
|
+
schemaVersion: "0.1.0",
|
|
3573
|
+
policy: POLICY4,
|
|
3574
|
+
ledger: resolution.ledger,
|
|
3575
|
+
resolution: pin5(decisions),
|
|
3576
|
+
receipt: pin5(files.get("receipt.json"))
|
|
3577
|
+
});
|
|
3578
|
+
add("intent.json", intent);
|
|
3579
|
+
return { files, dirs, receipt };
|
|
3580
|
+
}
|
|
3581
|
+
function inspect(root, plan, complete) {
|
|
3582
|
+
directory4(root);
|
|
3583
|
+
const children = /* @__PURE__ */ new Map();
|
|
3584
|
+
for (const dir of plan.dirs) children.set(dir, /* @__PURE__ */ new Set());
|
|
3585
|
+
for (const path of [...plan.dirs, ...plan.files.keys()]) {
|
|
3586
|
+
if (!path) continue;
|
|
3587
|
+
const slash = path.lastIndexOf("/");
|
|
3588
|
+
children.get(slash < 0 ? "" : path.slice(0, slash)).add(path.slice(slash + 1));
|
|
3589
|
+
}
|
|
3590
|
+
for (const dir of plan.dirs) {
|
|
3591
|
+
const path = join8(root, dir);
|
|
3592
|
+
if (!stat(path)) {
|
|
3593
|
+
if (complete)
|
|
3594
|
+
throw new Error("Completed application is missing a directory");
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
directory4(path);
|
|
3598
|
+
if (dir !== ".pending" && readdirSync4(path).some((name2) => !children.get(dir).has(name2)))
|
|
3599
|
+
throw new Error("Application conflict: unexpected entries");
|
|
3600
|
+
}
|
|
3601
|
+
for (const [path, bytes2] of plan.files) {
|
|
3602
|
+
if (stat(join8(root, path))) exact(join8(root, path), bytes2);
|
|
3603
|
+
else if (complete)
|
|
3604
|
+
throw new Error(`Completed application is missing ${path}`);
|
|
3605
|
+
}
|
|
3606
|
+
const pending = join8(root, ".pending");
|
|
3607
|
+
if (stat(pending)) {
|
|
3608
|
+
const names = readdirSync4(pending);
|
|
3609
|
+
if (names.length > 64)
|
|
3610
|
+
throw new Error("Application staging-file budget exceeded");
|
|
3611
|
+
const expected = new Map(
|
|
3612
|
+
[...plan.files.values()].map((bytes2) => [pin5(bytes2).sha256, bytes2])
|
|
3613
|
+
);
|
|
3614
|
+
let total = 0;
|
|
3615
|
+
for (const name2 of names) {
|
|
3616
|
+
if (!/^[a-f0-9]{64}-[a-f0-9-]{36}$/.test(name2))
|
|
3617
|
+
throw new Error("Unexpected application staging file");
|
|
3618
|
+
const bytes2 = expected.get(name2.slice(0, 64));
|
|
3619
|
+
if (!bytes2) throw new Error("Unknown staged content");
|
|
3620
|
+
const path = join8(pending, name2), s = stat(path);
|
|
3621
|
+
if (!s) continue;
|
|
3622
|
+
if (!s.isFile() || s.mode & 63)
|
|
3623
|
+
throw new Error("Invalid staging file");
|
|
3624
|
+
total += s.size;
|
|
3625
|
+
if (total > LIMIT3)
|
|
3626
|
+
throw new Error("Application staging-byte budget exceeded");
|
|
3627
|
+
let partial;
|
|
3628
|
+
try {
|
|
3629
|
+
partial = readLocal(path, bytes2.byteLength);
|
|
3630
|
+
} catch (error2) {
|
|
3631
|
+
if (error2.code === "ENOENT") continue;
|
|
3632
|
+
throw error2;
|
|
3633
|
+
}
|
|
3634
|
+
if (!Buffer.from(bytes2).subarray(0, partial.byteLength).equals(Buffer.from(partial)))
|
|
3635
|
+
throw new Error("Conflicting staged bytes");
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
function publish(root, path, bytes2) {
|
|
3640
|
+
const target2 = join8(root, path);
|
|
3641
|
+
if (stat(target2)) {
|
|
3642
|
+
exact(target2, bytes2);
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
const temporary = join8(
|
|
3646
|
+
root,
|
|
3647
|
+
".pending",
|
|
3648
|
+
`${pin5(bytes2).sha256}-${randomUUID2()}`
|
|
3649
|
+
);
|
|
3650
|
+
let fd;
|
|
3651
|
+
try {
|
|
3652
|
+
fd = openSync2(temporary, "wx", 384);
|
|
3653
|
+
writeFileSync6(fd, bytes2);
|
|
3654
|
+
fsyncSync(fd);
|
|
3655
|
+
closeSync2(fd);
|
|
3656
|
+
fd = void 0;
|
|
3657
|
+
try {
|
|
3658
|
+
linkSync(temporary, target2);
|
|
3659
|
+
} catch (error2) {
|
|
3660
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
3661
|
+
exact(target2, bytes2);
|
|
3662
|
+
}
|
|
3663
|
+
} finally {
|
|
3664
|
+
if (fd !== void 0) closeSync2(fd);
|
|
3665
|
+
if (stat(temporary)) unlinkSync(temporary);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
function verifiedMigrationApplication(root) {
|
|
3669
|
+
directory4(root);
|
|
3670
|
+
const plan = prepare(
|
|
3671
|
+
join8(root, "ledger"),
|
|
3672
|
+
readLocal(join8(root, "resolution.json"), 2 * 1024 * 1024)
|
|
3673
|
+
);
|
|
3674
|
+
inspect(root, plan, true);
|
|
3675
|
+
return plan;
|
|
3676
|
+
}
|
|
3677
|
+
function applyMigration(args2) {
|
|
3678
|
+
if (args2.flags["help"]) {
|
|
3679
|
+
console.log(
|
|
3680
|
+
"migrate apply <ledger> <decisions.json> --out <store> [--json]\nmigrate apply-verify <store> [--json]\nPrivate local application only; Index read paths remain unchanged."
|
|
3681
|
+
);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
try {
|
|
3685
|
+
const verify = args2.subcommand === "apply-verify", out = args2.flags["out"];
|
|
3686
|
+
if (verify ? args2.positional.length !== 1 || Object.keys(args2.flags).some((key) => key !== "json") : args2.positional.length !== 2 || typeof out !== "string" || Object.keys(args2.flags).some((key) => !["out", "json"].includes(key)))
|
|
3687
|
+
throw new Error(
|
|
3688
|
+
"Usage: migrate apply <ledger> <decisions.json> --out <store>, or migrate apply-verify <store>"
|
|
3689
|
+
);
|
|
3690
|
+
const root = resolve10(verify ? args2.positional[0] : out);
|
|
3691
|
+
if (verify) directory4(root);
|
|
3692
|
+
const plan = prepare(
|
|
3693
|
+
verify ? join8(root, "ledger") : resolve10(args2.positional[0]),
|
|
3694
|
+
readLocal(
|
|
3695
|
+
verify ? join8(root, "resolution.json") : args2.positional[1],
|
|
3696
|
+
2 * 1024 * 1024
|
|
3697
|
+
)
|
|
3698
|
+
);
|
|
3699
|
+
let created = false;
|
|
3700
|
+
if (!verify) {
|
|
3701
|
+
try {
|
|
3702
|
+
mkdirSync5(root, { mode: 448 });
|
|
3703
|
+
created = true;
|
|
3704
|
+
} catch (error2) {
|
|
3705
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
if (created) {
|
|
3709
|
+
mkdirSync5(join8(root, ".pending"), { mode: 448 });
|
|
3710
|
+
publish(root, "intent.json", plan.files.get("intent.json"));
|
|
3711
|
+
} else exact(join8(root, "intent.json"), plan.files.get("intent.json"));
|
|
3712
|
+
const completed = !!stat(join8(root, "receipt.json"));
|
|
3713
|
+
inspect(root, plan, verify || completed);
|
|
3714
|
+
if (!verify && !completed) {
|
|
3715
|
+
for (const dir of plan.dirs)
|
|
3716
|
+
if (dir) {
|
|
3717
|
+
try {
|
|
3718
|
+
mkdirSync5(join8(root, dir), { mode: 448 });
|
|
3719
|
+
} catch (error2) {
|
|
3720
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
3721
|
+
directory4(join8(root, dir));
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
for (const [path, bytes2] of plan.files)
|
|
3725
|
+
if (path !== "receipt.json") publish(root, path, bytes2);
|
|
3726
|
+
inspect(root, plan, false);
|
|
3727
|
+
publish(root, "receipt.json", plan.files.get("receipt.json"));
|
|
3728
|
+
}
|
|
3729
|
+
inspect(root, plan, true);
|
|
3730
|
+
const result = {
|
|
3731
|
+
success: true,
|
|
3732
|
+
action: verify ? "verified" : completed ? "reused" : created ? "applied" : "resumed",
|
|
3733
|
+
directory: root,
|
|
3734
|
+
receipt: pin5(plan.files.get("receipt.json")),
|
|
3735
|
+
counts: plan.receipt.counts,
|
|
3736
|
+
target: plan.receipt.target,
|
|
3737
|
+
readPath: "not_activated",
|
|
3738
|
+
verification: "current_code_replay"
|
|
3739
|
+
};
|
|
3740
|
+
if (args2.flags["json"]) console.log(JSON.stringify(result, null, 2));
|
|
3741
|
+
else
|
|
3742
|
+
console.log(
|
|
3743
|
+
`Local migration ${result.action}: ${result.counts.appliedCandidates} candidates, ${result.counts.retainedHistoricalRows} historical rows retained, 0 Index imports.
|
|
3744
|
+
${root}`
|
|
3745
|
+
);
|
|
3746
|
+
} catch (error2) {
|
|
3747
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3748
|
+
if (args2.flags["json"])
|
|
3749
|
+
console.log(
|
|
3750
|
+
JSON.stringify({
|
|
3751
|
+
success: false,
|
|
3752
|
+
stage: "migration_application",
|
|
3753
|
+
error: message
|
|
3754
|
+
})
|
|
3755
|
+
);
|
|
3756
|
+
else console.error(message);
|
|
3757
|
+
process.exitCode = 2;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
// src/commands/migration-read-path.ts
|
|
3762
|
+
var HASH = /^[a-f0-9]{64}$/;
|
|
3763
|
+
var LIMIT4 = 16384;
|
|
3764
|
+
var MAX_EVENTS = 1024;
|
|
3765
|
+
var bytes = (value) => Buffer.from(JSON.stringify(value, null, 2) + "\n");
|
|
3766
|
+
var pin6 = (value) => ({
|
|
3767
|
+
sha256: createHash14("sha256").update(value).digest("hex"),
|
|
3768
|
+
byteLength: value.byteLength
|
|
3769
|
+
});
|
|
3770
|
+
var name = (sequence) => `${String(sequence).padStart(6, "0")}.json`;
|
|
3771
|
+
function privateDirectory2(path) {
|
|
3772
|
+
const stat2 = lstatSync9(path);
|
|
3773
|
+
if (!stat2.isDirectory() || stat2.mode & 63)
|
|
3774
|
+
throw new Error("Selector requires private real directories");
|
|
3775
|
+
}
|
|
3776
|
+
function privateFile(path) {
|
|
3777
|
+
const stat2 = lstatSync9(path);
|
|
3778
|
+
if (!stat2.isFile() || stat2.mode & 63)
|
|
3779
|
+
throw new Error("Selector requires private regular files");
|
|
3780
|
+
return readLocal(path, LIMIT4);
|
|
3781
|
+
}
|
|
3782
|
+
function object3(value) {
|
|
3783
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3784
|
+
throw new Error("Invalid read transition object");
|
|
3785
|
+
return value;
|
|
3786
|
+
}
|
|
3787
|
+
function target(value) {
|
|
3788
|
+
const v = object3(value);
|
|
3789
|
+
if (v.kind === "legacy") return { kind: "legacy" };
|
|
3790
|
+
const p = object3(v.receipt);
|
|
3791
|
+
if (v.kind !== "local_store" || typeof v.directory !== "string" || v.directory.length > 4096 || !isAbsolute(v.directory) || resolve11(v.directory) !== v.directory || typeof p.sha256 !== "string" || !HASH.test(p.sha256) || !Number.isSafeInteger(p.byteLength) || p.byteLength < 1 || p.byteLength > 256 * 1024 * 1024)
|
|
3792
|
+
throw new Error("Invalid pinned local store target");
|
|
3793
|
+
return {
|
|
3794
|
+
kind: "local_store",
|
|
3795
|
+
directory: v.directory,
|
|
3796
|
+
receipt: { sha256: p.sha256, byteLength: p.byteLength }
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function history(root) {
|
|
3800
|
+
privateDirectory2(root);
|
|
3801
|
+
privateDirectory2(join9(root, ".pending"));
|
|
3802
|
+
const names = readdirSync5(root).filter((v) => v !== ".pending").sort();
|
|
3803
|
+
if (!names.length || names.length > MAX_EVENTS || names.some((v, i) => v !== name(i)))
|
|
3804
|
+
throw new Error(
|
|
3805
|
+
"Selector history is missing, noncontiguous or exceeds its budget"
|
|
3806
|
+
);
|
|
3807
|
+
const pending = readdirSync5(join9(root, ".pending"));
|
|
3808
|
+
if (pending.length > 64) throw new Error("Selector staging budget exceeded");
|
|
3809
|
+
for (const n of pending) {
|
|
3810
|
+
if (!/^[a-f0-9-]{36}$/.test(n))
|
|
3811
|
+
throw new Error("Unexpected selector staging entry");
|
|
3812
|
+
try {
|
|
3813
|
+
privateFile(join9(root, ".pending", n));
|
|
3814
|
+
} catch (error2) {
|
|
3815
|
+
if (error2.code !== "ENOENT") throw error2;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
const entries = [];
|
|
3819
|
+
for (const [i, n] of names.entries()) {
|
|
3820
|
+
const raw = privateFile(join9(root, n));
|
|
3821
|
+
const v = object3(reviewedJson(raw));
|
|
3822
|
+
const t = target(v.target);
|
|
3823
|
+
const previous = entries.at(-1)?.id ?? null;
|
|
3824
|
+
let action;
|
|
3825
|
+
let rollbackTo = null;
|
|
3826
|
+
if (i === 0 && v.action === "initialize" && t.kind === "legacy")
|
|
3827
|
+
action = "initialize";
|
|
3828
|
+
else if (i > 0 && v.action === "activate" && t.kind === "local_store")
|
|
3829
|
+
action = "activate";
|
|
3830
|
+
else if (i > 0 && v.action === "rollback" && typeof v.rollbackTo === "string") {
|
|
3831
|
+
const earlier = entries.find((e) => e.id === v.rollbackTo);
|
|
3832
|
+
if (!earlier || !bytes(earlier.event.target).equals(bytes(t)))
|
|
3833
|
+
throw new Error("Rollback must restore an earlier recorded target");
|
|
3834
|
+
action = "rollback";
|
|
3835
|
+
rollbackTo = v.rollbackTo;
|
|
3836
|
+
} else throw new Error("Invalid read transition action");
|
|
3837
|
+
const event = {
|
|
3838
|
+
kind: "legacy_migration_read_transition",
|
|
3839
|
+
schemaVersion: "0.1.0",
|
|
3840
|
+
sequence: i,
|
|
3841
|
+
previous,
|
|
3842
|
+
action,
|
|
3843
|
+
target: t,
|
|
3844
|
+
rollbackTo
|
|
3845
|
+
};
|
|
3846
|
+
if (!bytes(event).equals(Buffer.from(raw)))
|
|
3847
|
+
throw new Error("Changed or noncanonical selector history");
|
|
3848
|
+
entries.push({ id: pin6(raw).sha256, event });
|
|
3849
|
+
}
|
|
3850
|
+
return entries;
|
|
3851
|
+
}
|
|
3852
|
+
function verifiedTarget(t) {
|
|
3853
|
+
if (t.kind === "legacy") return [];
|
|
3854
|
+
const plan = verifiedMigrationApplication(t.directory);
|
|
3855
|
+
if (!bytes(pin6(plan.files.get("receipt.json"))).equals(bytes(t.receipt)))
|
|
3856
|
+
throw new Error("Selected store receipt does not match its activation pin");
|
|
3857
|
+
return plan.receipt.candidates.map((c) => ({
|
|
3858
|
+
identity: c.identity,
|
|
3859
|
+
revision: c.revision,
|
|
3860
|
+
candidate: JSON.parse(
|
|
3861
|
+
Buffer.from(plan.files.get(c.path)).toString("utf8")
|
|
3862
|
+
)
|
|
3863
|
+
}));
|
|
3864
|
+
}
|
|
3865
|
+
function publish2(root, event) {
|
|
3866
|
+
const raw = bytes(event);
|
|
3867
|
+
if (raw.length > LIMIT4)
|
|
3868
|
+
throw new Error("Read transition exceeds byte budget");
|
|
3869
|
+
const temporary = join9(root, ".pending", randomUUID3());
|
|
3870
|
+
let fd;
|
|
3871
|
+
try {
|
|
3872
|
+
fd = openSync3(temporary, "wx", 384);
|
|
3873
|
+
writeFileSync7(fd, raw);
|
|
3874
|
+
fsyncSync2(fd);
|
|
3875
|
+
closeSync3(fd);
|
|
3876
|
+
fd = void 0;
|
|
3877
|
+
linkSync2(temporary, join9(root, name(event.sequence)));
|
|
3878
|
+
} finally {
|
|
3879
|
+
if (fd !== void 0) closeSync3(fd);
|
|
3880
|
+
try {
|
|
3881
|
+
unlinkSync2(temporary);
|
|
3882
|
+
} catch (error2) {
|
|
3883
|
+
if (error2.code !== "ENOENT") throw error2;
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function migrationReadPath(args2) {
|
|
3888
|
+
const usage2 = "migrate read-init --out <selector> [--json]\nmigrate read-path <selector> [--json]\nmigrate read-activate <selector> <completed-store> --id <expected-head> [--json]\nmigrate read-rollback <selector> <earlier-head> --id <expected-head> [--json]";
|
|
3889
|
+
if (args2.flags["help"]) {
|
|
3890
|
+
console.log(usage2);
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
let currentHead;
|
|
3894
|
+
try {
|
|
3895
|
+
const init = args2.subcommand === "read-init", read = args2.subcommand === "read-path";
|
|
3896
|
+
const allowed = init ? ["out", "json"] : read ? ["json"] : ["id", "json"];
|
|
3897
|
+
if (Object.keys(args2.flags).some((k) => !allowed.includes(k)) || args2.positional.length !== (init ? 0 : read ? 1 : 2) || init && typeof args2.flags["out"] !== "string" || !init && !read && (typeof args2.flags["id"] !== "string" || !HASH.test(args2.flags["id"])))
|
|
3898
|
+
throw new Error(usage2);
|
|
3899
|
+
const root = resolve11(
|
|
3900
|
+
init ? args2.flags["out"] : args2.positional[0]
|
|
3901
|
+
);
|
|
3902
|
+
if (init) {
|
|
3903
|
+
mkdirSync6(root, { mode: 448 });
|
|
3904
|
+
mkdirSync6(join9(root, ".pending"), { mode: 448 });
|
|
3905
|
+
publish2(root, {
|
|
3906
|
+
kind: "legacy_migration_read_transition",
|
|
3907
|
+
schemaVersion: "0.1.0",
|
|
3908
|
+
sequence: 0,
|
|
3909
|
+
previous: null,
|
|
3910
|
+
action: "initialize",
|
|
3911
|
+
target: { kind: "legacy" },
|
|
3912
|
+
rollbackTo: null
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
let entries = history(root);
|
|
3916
|
+
currentHead = entries.at(-1).id;
|
|
3917
|
+
if (!init && !read) {
|
|
3918
|
+
const head2 = entries.at(-1);
|
|
3919
|
+
if (args2.flags["id"] !== head2.id)
|
|
3920
|
+
throw new Error(
|
|
3921
|
+
"Stale expected read head; inspect the current selector before retrying"
|
|
3922
|
+
);
|
|
3923
|
+
if (entries.length >= MAX_EVENTS)
|
|
3924
|
+
throw new Error("Selector transition budget exceeded");
|
|
3925
|
+
let t;
|
|
3926
|
+
let rollbackTo = null;
|
|
3927
|
+
if (args2.subcommand === "read-activate") {
|
|
3928
|
+
const directory5 = resolve11(args2.positional[1]);
|
|
3929
|
+
const plan = verifiedMigrationApplication(directory5);
|
|
3930
|
+
t = {
|
|
3931
|
+
kind: "local_store",
|
|
3932
|
+
directory: directory5,
|
|
3933
|
+
receipt: pin6(plan.files.get("receipt.json"))
|
|
3934
|
+
};
|
|
3935
|
+
} else {
|
|
3936
|
+
const earlier = entries.slice(0, -1).find((e) => e.id === args2.positional[1]);
|
|
3937
|
+
if (!earlier)
|
|
3938
|
+
throw new Error(
|
|
3939
|
+
"Rollback target must be an earlier head in this selector"
|
|
3940
|
+
);
|
|
3941
|
+
t = earlier.event.target;
|
|
3942
|
+
rollbackTo = earlier.id;
|
|
3943
|
+
verifiedTarget(t);
|
|
3944
|
+
}
|
|
3945
|
+
publish2(root, {
|
|
3946
|
+
kind: "legacy_migration_read_transition",
|
|
3947
|
+
schemaVersion: "0.1.0",
|
|
3948
|
+
sequence: entries.length,
|
|
3949
|
+
previous: head2.id,
|
|
3950
|
+
action: rollbackTo ? "rollback" : "activate",
|
|
3951
|
+
target: t,
|
|
3952
|
+
rollbackTo
|
|
3953
|
+
});
|
|
3954
|
+
entries = history(root);
|
|
3955
|
+
}
|
|
3956
|
+
const head = entries.at(-1);
|
|
3957
|
+
currentHead = head.id;
|
|
3958
|
+
const candidates = verifiedTarget(head.event.target);
|
|
3959
|
+
const result = {
|
|
3960
|
+
success: true,
|
|
3961
|
+
scope: "private_local_read_selector",
|
|
3962
|
+
head: head.id,
|
|
3963
|
+
target: head.event.target,
|
|
3964
|
+
history: entries,
|
|
3965
|
+
candidates,
|
|
3966
|
+
indexImported: 0,
|
|
3967
|
+
scientificValidity: "not_checked",
|
|
3968
|
+
decisionIdentity: "not_authenticated",
|
|
3969
|
+
verification: "current_code_replay"
|
|
3970
|
+
};
|
|
3971
|
+
if (args2.flags["json"]) console.log(JSON.stringify(result, null, 2));
|
|
3972
|
+
else
|
|
3973
|
+
console.log(
|
|
3974
|
+
`Local read path: ${head.event.target.kind}; ${candidates.length} verified candidates.
|
|
3975
|
+
Head: ${head.id}
|
|
3976
|
+
Index read path unchanged.`
|
|
3977
|
+
);
|
|
3978
|
+
} catch (error2) {
|
|
3979
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3980
|
+
if (args2.flags["json"])
|
|
3981
|
+
console.log(
|
|
3982
|
+
JSON.stringify({
|
|
3983
|
+
success: false,
|
|
3984
|
+
stage: "migration_read_path",
|
|
3985
|
+
head: currentHead,
|
|
3986
|
+
error: message
|
|
3987
|
+
})
|
|
3988
|
+
);
|
|
3989
|
+
else console.error(message);
|
|
3990
|
+
process.exitCode = 2;
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
// src/commands/migrate.ts
|
|
3995
|
+
function migrate(args2) {
|
|
3996
|
+
if (args2.subcommand === "compatibility") {
|
|
3997
|
+
migrationCompatibility(args2);
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
if (args2.subcommand === "legacy-scores") {
|
|
4001
|
+
legacyScores(args2);
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
if (["read-init", "read-path", "read-activate", "read-rollback"].includes(
|
|
4005
|
+
args2.subcommand
|
|
4006
|
+
)) {
|
|
4007
|
+
migrationReadPath(args2);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
if (args2.subcommand === "apply" || args2.subcommand === "apply-verify") {
|
|
4011
|
+
applyMigration(args2);
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
if (args2.subcommand === "resolve") {
|
|
4015
|
+
resolveMigration(args2);
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
if (args2.subcommand === "ledger" || args2.subcommand === "ledger-verify") {
|
|
4019
|
+
migrationLedger(args2);
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
if (args2.subcommand === "export" || args2.subcommand === "verify") {
|
|
4023
|
+
exportMigration(args2);
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
if (args2.subcommand === "review") {
|
|
4027
|
+
reviewMigration(args2);
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
if (args2.flags["help"]) {
|
|
4031
|
+
console.log(
|
|
4032
|
+
"disclosureos migrate dry-run <legacy.json> --id <source-namespace> [--json]\nFor explicit mappings: disclosureos migrate review <legacy.json> <review.json> [--json]\nExport: disclosureos migrate export <legacy.json> <review.json> --out <bundle> [--json]\nVerify: disclosureos migrate verify <bundle> [--json]\nLedger: disclosureos migrate ledger <bundle>... --out <ledger> [--json]\nVerify ledger: disclosureos migrate ledger-verify <ledger> [--json]\nResolve: disclosureos migrate resolve <ledger> <decisions.json> [--json]\nApply locally: disclosureos migrate apply <ledger> <decisions.json> --out <store> [--json]\nVerify application: disclosureos migrate apply-verify <store> [--json]\nRead selector: disclosureos migrate read-path <selector> [--json]\nSee migrate read-init --help for activation and rollback.\nPreserve historical scores: disclosureos migrate legacy-scores <scores.json> <plan.json> [--json]\nField inventory and sensor review: disclosureos migrate compatibility <legacy.json> [sensor-review.json] --id <namespace> [--json]\nExperimental local artifacts only; no Index database writes."
|
|
4033
|
+
);
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
try {
|
|
4037
|
+
const namespace = args2.flags["id"];
|
|
4038
|
+
if (args2.subcommand !== "dry-run" || args2.positional.length !== 1 || typeof namespace !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(namespace) || Object.keys(args2.flags).some((key) => !["id", "json"].includes(key))) {
|
|
4039
|
+
throw new Error(
|
|
4040
|
+
"Usage: disclosureos migrate dry-run <legacy.json> --id <source-namespace> [--json]"
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
const bytes2 = readLocal(args2.positional[0], 8 * 1024 * 1024);
|
|
4044
|
+
const report = buildMigrationReport(bytes2, namespace);
|
|
4045
|
+
if (args2.flags["json"]) console.log(JSON.stringify(report, null, 2));
|
|
4046
|
+
else {
|
|
4047
|
+
console.log(
|
|
4048
|
+
`Experimental migration dry run: ${report.counts.input} inputs, ${report.counts.candidates} review candidates, ${report.counts.quarantined} quarantined, 0 migrated.`
|
|
4049
|
+
);
|
|
4050
|
+
for (const row of report.records) {
|
|
4051
|
+
console.log(
|
|
4052
|
+
JSON.stringify({
|
|
4053
|
+
sourcePointer: row.sourcePointer,
|
|
4054
|
+
sourceId: row.sourceId,
|
|
4055
|
+
decision: row.decision,
|
|
4056
|
+
unresolvedFields: row.mapping.filter(
|
|
4057
|
+
(field) => field.disposition === "unresolved"
|
|
4058
|
+
).length,
|
|
4059
|
+
reasons: row.reasons
|
|
4060
|
+
})
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
4063
|
+
console.log(
|
|
4064
|
+
"Use --json for exact source bytes, retained legacy data, candidates and field mappings. Every candidate requires explicit mapping review."
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
if (report.counts.quarantined > 0) process.exitCode = 1;
|
|
4068
|
+
} catch (error2) {
|
|
4069
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4070
|
+
if (args2.flags["json"])
|
|
4071
|
+
console.log(
|
|
4072
|
+
JSON.stringify({ success: false, stage: "input", error: message })
|
|
4073
|
+
);
|
|
4074
|
+
else console.error(message);
|
|
4075
|
+
process.exitCode = 2;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
// src/commands/completeness.ts
|
|
4080
|
+
import { parseEnrichedObservation as parseEnrichedObservation3 } from "@disclosureos/schema";
|
|
4081
|
+
import { getPopulationCoverage } from "@disclosureos/scoring";
|
|
4082
|
+
function completeness(args2) {
|
|
4083
|
+
const population = args2.command === "population-coverage";
|
|
4084
|
+
if (args2.flags["help"]) {
|
|
4085
|
+
printUsage4(population);
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
const targets = [args2.subcommand, ...args2.positional].filter((t) => !!t);
|
|
4089
|
+
if (targets.length === 0) {
|
|
4090
|
+
printUsage4(population);
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
const recursive = !!args2.flags["recursive"];
|
|
4094
|
+
const jsonMode = !!args2.flags["json"];
|
|
4095
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4096
|
+
const files = [];
|
|
4097
|
+
const emptyTargets = [];
|
|
4098
|
+
for (const target2 of targets) {
|
|
4099
|
+
const found = findJSONFiles(target2, recursive);
|
|
4100
|
+
if (found.length === 0) {
|
|
4101
|
+
emptyTargets.push(target2);
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
for (const f of found) {
|
|
4105
|
+
if (!seen.has(f)) {
|
|
4106
|
+
seen.add(f);
|
|
4107
|
+
files.push(f);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
if (jsonMode) {
|
|
4112
|
+
runJsonMode2(files, emptyTargets);
|
|
4113
|
+
return;
|
|
4114
|
+
}
|
|
4115
|
+
for (const t of emptyTargets) {
|
|
4116
|
+
console.log(error(`No JSON files found at: ${t}`));
|
|
4117
|
+
}
|
|
4118
|
+
if (files.length === 0) {
|
|
4119
|
+
process.exit(1);
|
|
4120
|
+
}
|
|
4121
|
+
let hasErrors = false;
|
|
4122
|
+
const percentages = [];
|
|
4123
|
+
const reqPercentages = [];
|
|
4124
|
+
for (const file of files) {
|
|
4125
|
+
const result = scoreFile(file, population);
|
|
4126
|
+
if (!result.valid) {
|
|
4127
|
+
hasErrors = true;
|
|
4128
|
+
continue;
|
|
4129
|
+
}
|
|
4130
|
+
percentages.push(result.percentage);
|
|
4131
|
+
reqPercentages.push(result.requiredPercentage);
|
|
4132
|
+
}
|
|
4133
|
+
console.log("");
|
|
4134
|
+
if (hasErrors || emptyTargets.length > 0) {
|
|
4135
|
+
console.log(error("Some files could not be scored (see errors above)."));
|
|
4136
|
+
process.exit(1);
|
|
4137
|
+
}
|
|
4138
|
+
const mean = avg(percentages);
|
|
4139
|
+
const reqMean = avg(reqPercentages);
|
|
4140
|
+
console.log(
|
|
4141
|
+
success(
|
|
4142
|
+
`${files.length} file(s) scored \u2014 mean ${population ? "population coverage" : "completeness"} ${mean}%, required fields ${reqMean}%`
|
|
4143
|
+
)
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
function avg(values) {
|
|
4147
|
+
return values.length > 0 ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : 0;
|
|
4148
|
+
}
|
|
4149
|
+
function scoreFile(filePath, population = false) {
|
|
4150
|
+
let data;
|
|
4151
|
+
try {
|
|
4152
|
+
data = readJSON(filePath);
|
|
4153
|
+
} catch {
|
|
4154
|
+
console.log(error(`Failed to parse: ${filePath}`));
|
|
4155
|
+
return emptyResult(filePath, "Failed to parse JSON");
|
|
4156
|
+
}
|
|
4157
|
+
if (!data || typeof data !== "object") {
|
|
4158
|
+
console.log(error(`${filePath}: not a valid JSON object`));
|
|
4159
|
+
return emptyResult(filePath, "Not a valid JSON object");
|
|
4160
|
+
}
|
|
4161
|
+
const parsed = parseEnrichedObservation3(data);
|
|
4162
|
+
if (!parsed.success) {
|
|
4163
|
+
console.log(error(`${filePath}: invalid observation (${parsed.issues.length} issue${parsed.issues.length !== 1 ? "s" : ""})`));
|
|
4164
|
+
for (const issue of parsed.issues.slice(0, 5)) {
|
|
4165
|
+
console.log(` ${dim(`${issue.path}: ${issue.message}`)}`);
|
|
4166
|
+
}
|
|
4167
|
+
return emptyResult(filePath, "Invalid observation");
|
|
4168
|
+
}
|
|
4169
|
+
const comp = getPopulationCoverage(data);
|
|
4170
|
+
console.log(`
|
|
4171
|
+
${dim(filePath)}`);
|
|
4172
|
+
console.log(` ${population ? "Population coverage" : "Completeness"}: ${comp.percentage}% (${comp.present}/${comp.total} fields)`);
|
|
4173
|
+
console.log(` Required: ${comp.requiredPercentage}% (${comp.requiredPresent}/${comp.requiredTotal})`);
|
|
4174
|
+
if (comp.missing.length > 0) {
|
|
4175
|
+
const shown = comp.missing.slice(0, 10);
|
|
4176
|
+
console.log(` Missing (top ${shown.length}):`);
|
|
4177
|
+
for (const path of shown) {
|
|
4178
|
+
console.log(` ${dim("\u2022")} ${path}`);
|
|
4179
|
+
}
|
|
4180
|
+
if (comp.missing.length > shown.length) {
|
|
4181
|
+
console.log(` ${dim(`\u2026 and ${comp.missing.length - shown.length} more`)}`);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
return {
|
|
4185
|
+
file: filePath,
|
|
4186
|
+
valid: true,
|
|
4187
|
+
percentage: comp.percentage,
|
|
4188
|
+
requiredPercentage: comp.requiredPercentage,
|
|
4189
|
+
present: comp.present,
|
|
4190
|
+
total: comp.total,
|
|
4191
|
+
missing: comp.missing
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
function emptyResult(file, errorMsg) {
|
|
4195
|
+
return {
|
|
4196
|
+
file,
|
|
4197
|
+
valid: false,
|
|
4198
|
+
percentage: 0,
|
|
4199
|
+
requiredPercentage: 0,
|
|
4200
|
+
present: 0,
|
|
4201
|
+
total: 0,
|
|
4202
|
+
missing: [],
|
|
4203
|
+
error: errorMsg
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
function runJsonMode2(files, emptyTargets) {
|
|
4207
|
+
const fileResults = [];
|
|
4208
|
+
for (const file of files) {
|
|
4209
|
+
fileResults.push(scoreFileQuiet(file));
|
|
4210
|
+
}
|
|
4211
|
+
const validResults = fileResults.filter((r) => r.valid);
|
|
4212
|
+
const meanPercentage = avg(validResults.map((r) => r.percentage));
|
|
4213
|
+
const meanRequired = avg(validResults.map((r) => r.requiredPercentage));
|
|
4214
|
+
const allValid = fileResults.every((r) => r.valid) && emptyTargets.length === 0;
|
|
4215
|
+
const output = {
|
|
4216
|
+
files: fileResults,
|
|
4217
|
+
emptyTargets,
|
|
4218
|
+
totals: {
|
|
4219
|
+
files: files.length,
|
|
4220
|
+
meanPercentage,
|
|
4221
|
+
meanRequiredPercentage: meanRequired,
|
|
4222
|
+
emptyTargets: emptyTargets.length
|
|
4223
|
+
},
|
|
4224
|
+
valid: allValid
|
|
4225
|
+
};
|
|
4226
|
+
console.log(JSON.stringify(output, null, 2));
|
|
4227
|
+
if (!allValid) {
|
|
4228
|
+
process.exit(1);
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
function scoreFileQuiet(filePath) {
|
|
4232
|
+
let data;
|
|
4233
|
+
try {
|
|
4234
|
+
data = readJSON(filePath);
|
|
4235
|
+
} catch {
|
|
4236
|
+
return emptyResult(filePath, "Failed to parse JSON");
|
|
4237
|
+
}
|
|
4238
|
+
if (!data || typeof data !== "object") {
|
|
4239
|
+
return emptyResult(filePath, "Not a valid JSON object");
|
|
4240
|
+
}
|
|
4241
|
+
const parsed = parseEnrichedObservation3(data);
|
|
4242
|
+
if (!parsed.success) {
|
|
4243
|
+
return emptyResult(filePath, `Invalid observation (${parsed.issues.length} issue${parsed.issues.length !== 1 ? "s" : ""})`);
|
|
4244
|
+
}
|
|
4245
|
+
const comp = getPopulationCoverage(data);
|
|
4246
|
+
return {
|
|
4247
|
+
file: filePath,
|
|
4248
|
+
valid: true,
|
|
4249
|
+
percentage: comp.percentage,
|
|
4250
|
+
requiredPercentage: comp.requiredPercentage,
|
|
4251
|
+
present: comp.present,
|
|
4252
|
+
total: comp.total,
|
|
4253
|
+
missing: comp.missing
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
function printUsage4(population = false) {
|
|
4257
|
+
const command = population ? "population-coverage" : "completeness";
|
|
4258
|
+
console.log(heading(`${BRAND} ${command}`));
|
|
4259
|
+
console.log(`
|
|
4260
|
+
Measure how fully observation JSON files populate the records schema.
|
|
4261
|
+
`);
|
|
4262
|
+
console.log(`${dim("Usage:")}`);
|
|
4263
|
+
console.log(` disclosureos ${command} <path...> [options]
|
|
4264
|
+
`);
|
|
4265
|
+
console.log(`${dim("Paths:")}`);
|
|
4266
|
+
console.log(` Accepts one or more files, directories, or shell globs.
|
|
4267
|
+
`);
|
|
4268
|
+
console.log(`${dim("Options:")}`);
|
|
4269
|
+
console.log(` --recursive, -r Score all JSON files in directories recursively`);
|
|
4270
|
+
console.log(` --json, -j Output results as structured JSON (for CI and scripts)`);
|
|
4271
|
+
console.log(` --help, -h Show this help message
|
|
4272
|
+
`);
|
|
4273
|
+
if (population) console.log(" Counts field presence in the legacy schema, not analytical readiness or scientific quality.\n");
|
|
4274
|
+
console.log(`${dim("Scoring:")}`);
|
|
4275
|
+
console.log(` Files must be structurally valid observations before ${population ? "population coverage" : "completeness"} is measured.`);
|
|
4276
|
+
console.log(` Invalid JSON or invalid observations exit non-zero without a score.
|
|
4277
|
+
`);
|
|
4278
|
+
console.log(`${dim("Output:")}`);
|
|
4279
|
+
console.log(` \u2022 ${population ? "Population coverage" : "Completeness"} percentage (0\u2013100) \u2014 how many schema fields are populated`);
|
|
4280
|
+
console.log(` \u2022 Required field percentage \u2014 coverage of required fields only`);
|
|
4281
|
+
console.log(` \u2022 Top missing field paths \u2014 ${population ? "absent paths, without applicability or priority judgments" : "the next fields to target"}
|
|
4282
|
+
`);
|
|
4283
|
+
console.log(`${dim("Examples:")}`);
|
|
4284
|
+
console.log(` disclosureos ${command} ./data/nimitz.json`);
|
|
4285
|
+
console.log(` disclosureos ${command} ./data/ --recursive`);
|
|
4286
|
+
console.log(` disclosureos ${command} ./out/*.json --json
|
|
4287
|
+
`);
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
// src/commands/registry.ts
|
|
4291
|
+
import { deriveFieldPaths } from "@disclosureos/scoring";
|
|
4292
|
+
import {
|
|
4293
|
+
ALL_OBSERVABLES,
|
|
4294
|
+
TECHNOLOGY_OBSERVABLES,
|
|
4295
|
+
BIOLOGICS_OBSERVABLES,
|
|
4296
|
+
ASSESSMENT_LEVELS
|
|
4297
|
+
} from "@disclosureos/observables";
|
|
4298
|
+
import {
|
|
4299
|
+
OCS_TAXONOMY,
|
|
4300
|
+
getNode,
|
|
4301
|
+
getChildren,
|
|
4302
|
+
getTestableNodes,
|
|
4303
|
+
ORIGIN_DOMAINS,
|
|
4304
|
+
ORIGIN_DOMAIN_LABELS
|
|
4305
|
+
} from "@disclosureos/origins";
|
|
4306
|
+
function registry(args2) {
|
|
4307
|
+
if (args2.flags["help"]) {
|
|
4308
|
+
printUsage5();
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
4311
|
+
const sub = args2.subcommand;
|
|
4312
|
+
switch (sub) {
|
|
4313
|
+
case "fields":
|
|
4314
|
+
registryFields(args2);
|
|
4315
|
+
break;
|
|
4316
|
+
case "observables":
|
|
4317
|
+
registryObservables(args2);
|
|
4318
|
+
break;
|
|
4319
|
+
case "origins":
|
|
4320
|
+
registryOrigins(args2);
|
|
4321
|
+
break;
|
|
4322
|
+
default:
|
|
4323
|
+
printUsage5();
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
function registryFields(args2) {
|
|
4327
|
+
const group = args2.flags["category"];
|
|
4328
|
+
const paths = deriveFieldPaths();
|
|
4329
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4330
|
+
for (const field of paths) {
|
|
4331
|
+
const top = field.path.split(".")[0] ?? field.path;
|
|
4332
|
+
const list = groups.get(top) ?? [];
|
|
4333
|
+
list.push(field);
|
|
4334
|
+
groups.set(top, list);
|
|
4335
|
+
}
|
|
4336
|
+
console.log(heading("Observation Field Registry (schema-derived)"));
|
|
4337
|
+
if (group) {
|
|
4338
|
+
const fields2 = groups.get(group);
|
|
4339
|
+
if (!fields2 || fields2.length === 0) {
|
|
4340
|
+
console.log(`
|
|
4341
|
+
No fields found for group "${group}"`);
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
console.log(`
|
|
4345
|
+
Group: ${group}
|
|
4346
|
+
`);
|
|
4347
|
+
const rows = fields2.map((f) => [f.path, f.required ? "required" : "optional"]);
|
|
4348
|
+
console.log(table(rows));
|
|
4349
|
+
} else {
|
|
4350
|
+
for (const groupName of [...groups.keys()].sort()) {
|
|
4351
|
+
const fields2 = groups.get(groupName);
|
|
4352
|
+
console.log(`
|
|
4353
|
+
${groupName} (${fields2.length} fields)`);
|
|
4354
|
+
for (const f of fields2) {
|
|
4355
|
+
console.log(bullet(`${f.path}${f.required ? " [required]" : ""}`));
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
console.log(`
|
|
4360
|
+
${dim(`Total: ${paths.length} schema-derived field paths`)}
|
|
4361
|
+
`);
|
|
4362
|
+
}
|
|
4363
|
+
function registryObservables(args2) {
|
|
4364
|
+
const biologicsOnly = !!args2.flags["biologics"];
|
|
4365
|
+
const techOnly = !!args2.flags["technology"];
|
|
4366
|
+
console.log(heading("Observable Registry"));
|
|
4367
|
+
if (!biologicsOnly) {
|
|
4368
|
+
console.log(`
|
|
4369
|
+
Technology Observables (AATIP-derived)
|
|
4370
|
+
`);
|
|
4371
|
+
const techObs = Object.values(TECHNOLOGY_OBSERVABLES);
|
|
4372
|
+
for (const obs of techObs) {
|
|
4373
|
+
console.log(` ${obs.code} ${obs.label}`);
|
|
4374
|
+
if (obs.description) {
|
|
4375
|
+
console.log(` ${dim(obs.description)}`);
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
if (!techOnly) {
|
|
4380
|
+
console.log(`
|
|
4381
|
+
Biologics Observables (NHI Detection)
|
|
993
4382
|
`);
|
|
994
4383
|
const bioObs = Object.values(BIOLOGICS_OBSERVABLES);
|
|
995
4384
|
for (const obs of bioObs) {
|
|
@@ -1070,7 +4459,7 @@ ${label("ID", node.id)}`);
|
|
|
1070
4459
|
}
|
|
1071
4460
|
console.log("");
|
|
1072
4461
|
}
|
|
1073
|
-
function
|
|
4462
|
+
function printUsage5() {
|
|
1074
4463
|
console.log(heading(`${BRAND} registry`));
|
|
1075
4464
|
console.log(`
|
|
1076
4465
|
Introspect DisclosureOS field, observable, and origin registries.
|
|
@@ -1110,7 +4499,7 @@ import {
|
|
|
1110
4499
|
var FIELD_PATH_COUNT = deriveFieldPaths2().length;
|
|
1111
4500
|
function info(args2) {
|
|
1112
4501
|
if (args2.flags["help"]) {
|
|
1113
|
-
|
|
4502
|
+
printUsage6();
|
|
1114
4503
|
return;
|
|
1115
4504
|
}
|
|
1116
4505
|
const sub = args2.subcommand;
|
|
@@ -1257,7 +4646,7 @@ ${label("ID", node.id)}`);
|
|
|
1257
4646
|
}
|
|
1258
4647
|
console.log("");
|
|
1259
4648
|
}
|
|
1260
|
-
function
|
|
4649
|
+
function printUsage6() {
|
|
1261
4650
|
console.log(heading(`${BRAND} info`));
|
|
1262
4651
|
console.log(`
|
|
1263
4652
|
Quick reference for DisclosureOS types and definitions.
|
|
@@ -1291,6 +4680,25 @@ if (args.flags["version"]) {
|
|
|
1291
4680
|
case "validate":
|
|
1292
4681
|
validate(args);
|
|
1293
4682
|
break;
|
|
4683
|
+
case "migrate":
|
|
4684
|
+
migrate(args);
|
|
4685
|
+
break;
|
|
4686
|
+
case "profile":
|
|
4687
|
+
await profile(args);
|
|
4688
|
+
break;
|
|
4689
|
+
case "intake":
|
|
4690
|
+
await intake(args);
|
|
4691
|
+
break;
|
|
4692
|
+
case "dataset":
|
|
4693
|
+
await dataset(args);
|
|
4694
|
+
break;
|
|
4695
|
+
case "packet":
|
|
4696
|
+
await packet(args);
|
|
4697
|
+
break;
|
|
4698
|
+
case "manifest":
|
|
4699
|
+
manifest(args);
|
|
4700
|
+
break;
|
|
4701
|
+
case "population-coverage":
|
|
1294
4702
|
case "completeness":
|
|
1295
4703
|
completeness(args);
|
|
1296
4704
|
break;
|
|
@@ -1322,7 +4730,14 @@ function printHelp() {
|
|
|
1322
4730
|
console.log(`${dim("Commands:")}`);
|
|
1323
4731
|
console.log(` scaffold Generate typed data structure templates`);
|
|
1324
4732
|
console.log(` validate Validate observation JSON files`);
|
|
1325
|
-
console.log(`
|
|
4733
|
+
console.log(` manifest Validate sensor-manifest JSON files`);
|
|
4734
|
+
console.log(` packet Inspect, validate, or reproduce an experimental v2 packet`);
|
|
4735
|
+
console.log(` dataset Inspect or validate an experimental dataset release`);
|
|
4736
|
+
console.log(` intake Receive and verify unassessed source files locally`);
|
|
4737
|
+
console.log(` migrate Plan experimental v1-to-v2 migration without importing`);
|
|
4738
|
+
console.log(` profile List, inspect, or run explicit experimental profile checks`);
|
|
4739
|
+
console.log(` population-coverage Count populated legacy observation fields`);
|
|
4740
|
+
console.log(` completeness Legacy name for population coverage`);
|
|
1326
4741
|
console.log(` registry Introspect field, observable, and origin registries`);
|
|
1327
4742
|
console.log(` info Quick reference for types and definitions`);
|
|
1328
4743
|
console.log(` help Show this help message`);
|
|
@@ -1335,7 +4750,7 @@ function printHelp() {
|
|
|
1335
4750
|
console.log(`${dim("Examples:")}`);
|
|
1336
4751
|
console.log(` disclosureos scaffold observation --full`);
|
|
1337
4752
|
console.log(` disclosureos validate ./data/ --recursive`);
|
|
1338
|
-
console.log(` disclosureos
|
|
4753
|
+
console.log(` disclosureos population-coverage ./out/ --recursive`);
|
|
1339
4754
|
console.log(` disclosureos registry origins --id 1.1.3`);
|
|
1340
4755
|
console.log(` disclosureos info observable TO-3
|
|
1341
4756
|
`);
|
|
@@ -1345,8 +4760,8 @@ function printVersion() {
|
|
|
1345
4760
|
}
|
|
1346
4761
|
function readCliVersion() {
|
|
1347
4762
|
try {
|
|
1348
|
-
const packagePath =
|
|
1349
|
-
|
|
4763
|
+
const packagePath = join10(
|
|
4764
|
+
dirname6(fileURLToPath2(import.meta.url)),
|
|
1350
4765
|
"..",
|
|
1351
4766
|
"package.json"
|
|
1352
4767
|
);
|