arkaik 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/dist/index.js +757 -288
  2. package/dist/io.js +9 -4
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17210,6 +17210,83 @@ function signalTrippedInput(trip) {
17210
17210
  };
17211
17211
  }
17212
17212
 
17213
+ // ../schema/src/quality-regressions.ts
17214
+ var REGRESSION_SIGNALS = {
17215
+ "level-drop": "maturity on this cell does not regress",
17216
+ "new-severe-finding": "no open Critical or High finding on this cell",
17217
+ "reopened-finding": "a resolved finding stays resolved"
17218
+ };
17219
+ var cellKey = (criterionId, surface) => `${criterionId}::${surface}`;
17220
+ var rowsOf = (value) => Array.isArray(value) ? value : [];
17221
+ function assessmentsByCell(state) {
17222
+ const cells = /* @__PURE__ */ new Map();
17223
+ for (const assessment of rowsOf(state?.assessments)) {
17224
+ if (typeof assessment?.criterion_id !== "string" || typeof assessment?.surface !== "string") continue;
17225
+ cells.set(cellKey(assessment.criterion_id, assessment.surface), assessment);
17226
+ }
17227
+ return cells;
17228
+ }
17229
+ function severeByCell(state, library) {
17230
+ const cells = /* @__PURE__ */ new Map();
17231
+ for (const finding of rowsOf(state?.findings)) {
17232
+ if (typeof finding?.criterion_id !== "string" || typeof finding?.surface !== "string") continue;
17233
+ if (!isOpenFinding(finding)) continue;
17234
+ const severity = severityOf(finding, library);
17235
+ if (severity !== "critical" && severity !== "high") continue;
17236
+ const key = cellKey(finding.criterion_id, finding.surface);
17237
+ const existing = cells.get(key);
17238
+ if (existing === void 0) cells.set(key, [finding]);
17239
+ else existing.push(finding);
17240
+ }
17241
+ return cells;
17242
+ }
17243
+ function detectRegressions(previous, next, library) {
17244
+ const before = assessmentsByCell(previous);
17245
+ const after = assessmentsByCell(next);
17246
+ const comparable = (key) => before.has(key) && after.has(key);
17247
+ const regressions = [];
17248
+ for (const [key, current] of after) {
17249
+ const earlier = before.get(key);
17250
+ if (earlier === void 0) continue;
17251
+ if (!(Number(current.level) < Number(earlier.level))) continue;
17252
+ regressions.push({
17253
+ kind: "level-drop",
17254
+ criterion_id: current.criterion_id,
17255
+ surface: current.surface,
17256
+ signal: REGRESSION_SIGNALS["level-drop"],
17257
+ detail: `level ${earlier.level} \u2192 ${current.level} (${earlier.audit_id} \u2192 ${current.audit_id})`
17258
+ });
17259
+ }
17260
+ const severeBefore = severeByCell(previous, library);
17261
+ for (const [key, findings] of severeByCell(next, library)) {
17262
+ if (!comparable(key)) continue;
17263
+ if ((severeBefore.get(key) ?? []).length > 0) continue;
17264
+ for (const finding of findings) {
17265
+ regressions.push({
17266
+ kind: "new-severe-finding",
17267
+ criterion_id: finding.criterion_id,
17268
+ surface: finding.surface,
17269
+ signal: REGRESSION_SIGNALS["new-severe-finding"],
17270
+ detail: `${finding.id} \u2014 ${severityOf(finding, library)} (impact ${finding.impact} \xD7 likelihood ${finding.likelihood})`
17271
+ });
17272
+ }
17273
+ }
17274
+ const resolvedBefore = new Set(
17275
+ rowsOf(previous?.findings).filter((finding) => finding?.status === "resolved").map((finding) => finding.id)
17276
+ );
17277
+ for (const finding of rowsOf(next?.findings)) {
17278
+ if (!isOpenFinding(finding) || !resolvedBefore.has(finding.id)) continue;
17279
+ regressions.push({
17280
+ kind: "reopened-finding",
17281
+ criterion_id: finding.criterion_id,
17282
+ surface: finding.surface,
17283
+ signal: REGRESSION_SIGNALS["reopened-finding"],
17284
+ detail: `${finding.id} \u2014 "${finding.title}"`
17285
+ });
17286
+ }
17287
+ return regressions;
17288
+ }
17289
+
17213
17290
  // src/commands/init.ts
17214
17291
  var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
17215
17292
  var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
@@ -18576,18 +18653,288 @@ ${USAGE6}`);
18576
18653
  }
18577
18654
 
18578
18655
  // src/commands/pack.ts
18656
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
18657
+ import { dirname as dirname5, extname, resolve as resolve5 } from "node:path";
18658
+
18659
+ // src/lib/kritik-io.ts
18660
+ import { existsSync as existsSync7 } from "node:fs";
18661
+ import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
18662
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
18663
+
18664
+ // ../schema/src/cli/kritik-audit.ts
18665
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "node:fs";
18666
+ import { join as join4 } from "node:path";
18667
+
18668
+ // ../schema/src/cli/kritik-paths.ts
18579
18669
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
18580
- import { dirname as dirname3, extname, resolve as resolve3 } from "node:path";
18581
- var DEFAULT_BUNDLE_PATH5 = "docs/arkaik/bundle.json";
18582
- var USAGE7 = `arkaik pack [--no-journal] [--inline-assets] [--out <path>] [path]
18670
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
18671
+ var QUALITY_DIR = "docs/quality";
18672
+ var PROFILE_FILE = "profile.json";
18673
+ var OVERLAY_FILE = "criteria.custom.json";
18674
+ var AUDITS_DIR = "audits";
18675
+ var PACK_FILE = "library.json";
18676
+ function readJson(path6) {
18677
+ const text = readFileSync6(path6, "utf8");
18678
+ try {
18679
+ return JSON.parse(text);
18680
+ } catch (e) {
18681
+ throw new Error(`${path6}: not valid JSON \u2014 ${e.message}`);
18682
+ }
18683
+ }
18684
+ function writeJson(path6, value) {
18685
+ mkdirSync3(dirname3(path6), { recursive: true });
18686
+ writeFileSync5(path6, JSON.stringify(value, null, 2) + "\n");
18687
+ }
18688
+ var profilePath = (root) => join3(root, QUALITY_DIR, PROFILE_FILE);
18689
+ var overlayPath = (root) => join3(root, QUALITY_DIR, OVERLAY_FILE);
18690
+ var auditsDir = (root) => join3(root, QUALITY_DIR, AUDITS_DIR);
18691
+ var auditDir = (root, auditId) => join3(auditsDir(root), auditId);
18692
+ function loadProfile(root) {
18693
+ const path6 = profilePath(root);
18694
+ return existsSync5(path6) ? readJson(path6) : null;
18695
+ }
18696
+ function loadOverlay(root) {
18697
+ const path6 = overlayPath(root);
18698
+ return existsSync5(path6) ? readJson(path6) : null;
18699
+ }
18700
+
18701
+ // ../schema/src/cli/kritik-audit.ts
18702
+ var SCORES_FILE = "scores.json";
18703
+ var FINDINGS_FILE = "findings.json";
18704
+ var MATRIX_FILE = "matrix.json";
18705
+ var scoresPath = (root, auditId) => join4(auditDir(root, auditId), SCORES_FILE);
18706
+ var findingsPath = (root, auditId) => join4(auditDir(root, auditId), FINDINGS_FILE);
18707
+ var matrixPath = (root, auditId) => join4(auditDir(root, auditId), MATRIX_FILE);
18708
+ function listAuditIds(root) {
18709
+ const dir = auditsDir(root);
18710
+ if (!existsSync6(dir)) return [];
18711
+ return readdirSync2(dir).filter((name) => statSync(join4(dir, name)).isDirectory()).sort();
18712
+ }
18713
+ function newestAuditId(root) {
18714
+ const dir = auditsDir(root);
18715
+ if (!existsSync6(dir)) throw new Error(`no audits directory at ${dir}`);
18716
+ const ids = listAuditIds(root);
18717
+ if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
18718
+ return ids[ids.length - 1];
18719
+ }
18720
+ function loadScores(root, auditId) {
18721
+ const path6 = scoresPath(root, auditId);
18722
+ if (!existsSync6(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
18723
+ const file2 = readJson(path6);
18724
+ return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
18725
+ }
18726
+ function loadScoresOrEmpty(root, auditId) {
18727
+ return existsSync6(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
18728
+ }
18729
+ function loadFindings(root, auditId) {
18730
+ const path6 = findingsPath(root, auditId);
18731
+ if (!existsSync6(path6)) return { audit_id: auditId, findings: [] };
18732
+ const file2 = readJson(path6);
18733
+ return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
18734
+ }
18735
+ function saveScores(root, auditId, file2) {
18736
+ writeJson(scoresPath(root, auditId), file2);
18737
+ }
18738
+ function saveFindings(root, auditId, file2) {
18739
+ writeJson(findingsPath(root, auditId), file2);
18740
+ }
18741
+ function requireProfile(root) {
18742
+ const profile = loadProfile(root);
18743
+ if (!profile) {
18744
+ throw new Error(
18745
+ `no profile at ${join4(root, QUALITY_DIR, "profile.json")} \u2014 pick this project's surfaces first (\`arkaik kritik profile\`, or the plugin's init-profile.js).`
18746
+ );
18747
+ }
18748
+ return profile;
18749
+ }
18750
+ function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
18751
+ const findings = loadFindings(root, auditId);
18752
+ return {
18753
+ framework_version: scores.framework_version ?? library.version,
18754
+ profile: requireProfile(root),
18755
+ assessments: scores.assessments,
18756
+ findings: findings.findings
18757
+ };
18758
+ }
18759
+ function stripDerived(finding) {
18760
+ const { severity: _severity, priority: _priority, ...rest } = finding;
18761
+ void _severity;
18762
+ void _priority;
18763
+ return rest;
18764
+ }
18765
+ function requireAudit(root, auditId) {
18766
+ if (!listAuditIds(root).includes(auditId)) {
18767
+ throw new Error(`no audit "${auditId}" under ${auditsDir(root)}`);
18768
+ }
18769
+ }
18770
+ function loadCurrentQualitySection(root, library) {
18771
+ const auditIds = listAuditIds(root);
18772
+ if (auditIds.length === 0) return void 0;
18773
+ const profile = loadProfile(root);
18774
+ if (!profile) return void 0;
18775
+ const cells = /* @__PURE__ */ new Map();
18776
+ const findings = [];
18777
+ let frameworkVersion;
18778
+ for (const id of auditIds) {
18779
+ const scores = loadScoresOrEmpty(root, id);
18780
+ if (typeof scores.framework_version === "string") frameworkVersion = scores.framework_version;
18781
+ for (const assessment of scores.assessments) {
18782
+ cells.set(`${assessment.criterion_id}\0${assessment.surface}`, assessment);
18783
+ }
18784
+ for (const finding of loadFindings(root, id).findings) findings.push(stripDerived(finding));
18785
+ }
18786
+ return {
18787
+ framework_version: frameworkVersion ?? library.version,
18788
+ library,
18789
+ profile,
18790
+ assessments: [...cells.values()],
18791
+ findings
18792
+ };
18793
+ }
18794
+ function loadAuditQualitySection(root, auditId, library) {
18795
+ requireAudit(root, auditId);
18796
+ const section = loadQualitySection(root, auditId, library, loadScoresOrEmpty(root, auditId));
18797
+ return { ...section, library, findings: section.findings.map(stripDerived) };
18798
+ }
18799
+ function computeAuditMatrix(root, auditId, library) {
18800
+ const scores = loadScores(root, auditId);
18801
+ const section = loadQualitySection(root, auditId, library, scores);
18802
+ const matrix = deriveQualityMatrix({ quality: section }, library);
18803
+ const file2 = {
18804
+ audit_id: auditId,
18805
+ commit: scores.commit,
18806
+ framework_version: section.framework_version,
18807
+ matrix: matrix.matrix,
18808
+ overall: matrix.overall,
18809
+ finding_counts: matrix.finding_counts
18810
+ };
18811
+ writeJson(matrixPath(root, auditId), file2);
18812
+ return { section, matrix, file: file2 };
18813
+ }
18814
+ function renderMatrixMarkdown(matrix, domainNames) {
18815
+ const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
18816
+ const lines = [];
18817
+ lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
18818
+ lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
18819
+ for (const domain2 of matrix.domains) {
18820
+ const label = domainNames.get(domain2) ?? domain2;
18821
+ lines.push(
18822
+ `| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
18823
+ );
18824
+ }
18825
+ lines.push(
18826
+ `| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
18827
+ const score = matrix.overall[s];
18828
+ return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
18829
+ }).join(" | ")} |`
18830
+ );
18831
+ return lines.join("\n");
18832
+ }
18833
+ function locateFinding(root, id) {
18834
+ for (const auditId of [...listAuditIds(root)].reverse()) {
18835
+ const file2 = loadFindings(root, auditId);
18836
+ const finding = file2.findings.find((candidate) => candidate.id === id);
18837
+ if (finding) return { auditId, file: file2, finding };
18838
+ }
18839
+ return void 0;
18840
+ }
18841
+
18842
+ // src/lib/kritik-io.ts
18843
+ var KRITIK_ACTOR = "arkaik-cli";
18844
+ var DEFAULT_BUNDLE_PATH5 = join5("docs", "arkaik", "bundle.json");
18845
+ var VENDORED_PACK = join5(QUALITY_DIR, PACK_FILE);
18846
+ var BUNDLED_PACK = join5(dirname4(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
18847
+ function resolvePack(root) {
18848
+ const vendored = join5(root, VENDORED_PACK);
18849
+ if (existsSync7(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
18850
+ if (!existsSync7(BUNDLED_PACK)) {
18851
+ throw new Error(
18852
+ `no criteria pack found. Looked in:
18853
+ ${vendored}
18854
+ ${BUNDLED_PACK}
18855
+ The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
18856
+ );
18857
+ }
18858
+ return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
18859
+ }
18860
+ function loadKritikLibrary(root) {
18861
+ const pack = resolvePack(root);
18862
+ return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
18863
+ }
18864
+ function resolveJournal(root, bundlePath) {
18865
+ const resolved = bundlePath ?? join5(root, DEFAULT_BUNDLE_PATH5);
18866
+ return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync7(resolved) };
18867
+ }
18868
+ function appendQualityEvents(root, inputs, options = {}) {
18869
+ if (inputs.length === 0) return { events: [] };
18870
+ const actor = options.actor ?? KRITIK_ACTOR;
18871
+ const journal = resolveJournal(root, options.bundlePath);
18872
+ if (!journal.present) return { events: [] };
18873
+ const bundle = readBundle(journal.bundlePath);
18874
+ const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
18875
+ const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
18876
+ for (const event of events) appendJournalEvent(journal.journalPath, event);
18877
+ return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
18878
+ }
18879
+ function isQualitySection(value) {
18880
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18881
+ }
18882
+ function foldQualitySection(bundle, root, auditId) {
18883
+ const notFolded = (reason) => {
18884
+ const carriedSection = isQualitySection(bundle.quality);
18885
+ return {
18886
+ folded: false,
18887
+ carriedSection,
18888
+ notice: carriedSection ? `Quality: ${reason}
18889
+ Kept the quality section this bundle already carried \u2014 nothing replaced it.` : `Quality: ${reason}`
18890
+ };
18891
+ };
18892
+ if (auditId !== void 0) requireAudit(root, auditId);
18893
+ const auditIds = listAuditIds(root);
18894
+ if (auditIds.length === 0) {
18895
+ return notFolded(`none to fold \u2014 no audits under ${auditsDir(root)} (run \`arkaik kritik score\` to open one)`);
18896
+ }
18897
+ if (!loadProfile(root)) {
18898
+ return notFolded(`skipped, no profile \u2014 nothing at ${profilePath(root)} (run \`arkaik kritik profile\`)`);
18899
+ }
18900
+ let library;
18901
+ try {
18902
+ ({ library } = loadKritikLibrary(root));
18903
+ } catch (e) {
18904
+ return notFolded(`skipped, no pack \u2014 ${e.message}`);
18905
+ }
18906
+ const section = auditId === void 0 ? loadCurrentQualitySection(root, library) : loadAuditQualitySection(root, auditId, library);
18907
+ if (section === void 0) {
18908
+ return notFolded(`nothing to fold \u2014 no audits under ${auditsDir(root)}, or no profile at ${profilePath(root)}`);
18909
+ }
18910
+ bundle.quality = section;
18911
+ const count = auditId === void 0 ? auditIds.length : 1;
18912
+ return {
18913
+ folded: true,
18914
+ carriedSection: false,
18915
+ notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
18916
+ };
18917
+ }
18918
+ function resolveQualityRoot(cwd, filePath, root) {
18919
+ if (root !== void 0) return resolve4(cwd, root);
18920
+ const dir = dirname4(filePath);
18921
+ if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
18922
+ return cwd;
18923
+ }
18924
+
18925
+ // src/commands/pack.ts
18926
+ var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
18927
+ var USAGE7 = `arkaik pack [--no-journal] [--no-quality] [--inline-assets] [--audit <id>]
18928
+ [--root <dir>] [--out <path>] [path]
18583
18929
 
18584
18930
  Produce a single self-contained interchange bundle: fold in the sidecar
18585
- journal (or keep an existing embedded one) and, with --inline-assets, inline
18586
- local screenshot files as data: URIs. Written canonically via serializeBundle.
18587
- Unknown top-level keys and unknown fields always round-trip.
18931
+ journal (or keep an existing embedded one), fold docs/quality/ into the
18932
+ quality section, and, with --inline-assets, inline local screenshot files as
18933
+ data: URIs. Written canonically via serializeBundle. Unknown top-level keys
18934
+ and unknown fields always round-trip.
18588
18935
 
18589
18936
  Arguments:
18590
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH5}).
18937
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
18591
18938
 
18592
18939
  Options:
18593
18940
  --no-journal Omit the embedded journal[] (Publik-safe posture \u2014 history
@@ -18596,12 +18943,33 @@ Options:
18596
18943
  interchange) \u2014 embedded wins over the sidecar when the
18597
18944
  bundle already carries one, otherwise the sidecar is used
18598
18945
  (same precedence "arkaik validate" folds by).
18946
+ --no-quality DELETE the quality section rather than folding one in \u2014
18947
+ not merely "skip the fold", because the source bundle may
18948
+ already carry a section of its own and that one goes too.
18949
+ For any bundle that must not travel with open findings:
18950
+ a finding names an unfixed vulnerability and the file to
18951
+ find it in. ("arkaik push" packs this way by default;
18952
+ its --include-quality opts back in.)
18953
+ Default: docs/quality/ IS folded in.
18599
18954
  --inline-assets Convert relative-path metadata.platformScreenshots values
18600
18955
  into data: URIs by reading the file from disk (resolved
18601
18956
  against the bundle's directory). Absolute https:// URLs
18602
18957
  and existing data: URIs are left as-is. v1 scope: local
18603
18958
  files only \u2014 uploading a remote/hosted copy is not
18604
18959
  implemented.
18960
+ --audit <id> Pin ONE audit's snapshot instead of the default merge.
18961
+ They answer different questions: the merge (every audit,
18962
+ latest score per criterion x surface) answers "where does
18963
+ the product stand", while a pinned audit answers "how did
18964
+ that audit go" \u2014 the question its own matrix.json
18965
+ answers. An id that is not on disk is an error, not an
18966
+ empty section.
18967
+ --root <dir> Where docs/quality/ lives. Default: NOT the current
18968
+ directory \u2014 it is derived from the bundle's own path, so
18969
+ packing <repo>/docs/arkaik/bundle.json folds <repo>'s
18970
+ audits whatever directory you run from. Only a bundle
18971
+ kept outside that conventional layout falls back to the
18972
+ cwd, and that is the case this flag is for.
18605
18973
  --out <path> Write the packed bundle here instead of stdout.
18606
18974
  -h, --help Show this help.`;
18607
18975
  function fail7(message) {
@@ -18639,7 +19007,7 @@ function fatalResult2(bundlePath, message) {
18639
19007
  }
18640
19008
  function runPack(options = {}) {
18641
19009
  const cwd = options.cwd ?? process.cwd();
18642
- const filePath = resolve3(cwd, options.path ?? DEFAULT_BUNDLE_PATH5);
19010
+ const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
18643
19011
  const noJournal = options.noJournal ?? false;
18644
19012
  const inlineAssets = options.inlineAssets ?? false;
18645
19013
  let bundle;
@@ -18660,10 +19028,24 @@ function runPack(options = {}) {
18660
19028
  journalEventCount = events.length;
18661
19029
  }
18662
19030
  }
19031
+ let qualityFolded;
19032
+ let qualityNotice;
19033
+ if (options.noQuality ?? false) {
19034
+ delete bundle.quality;
19035
+ } else {
19036
+ const root = resolveQualityRoot(cwd, filePath, options.root);
19037
+ try {
19038
+ const fold = foldQualitySection(bundle, root, options.audit);
19039
+ qualityFolded = fold.folded;
19040
+ qualityNotice = fold.notice;
19041
+ } catch (e) {
19042
+ return fatalResult2(filePath, e.message);
19043
+ }
19044
+ }
18663
19045
  const inlinedAssets = [];
18664
19046
  const assetWarnings = [];
18665
19047
  if (inlineAssets) {
18666
- const bundleDir = dirname3(filePath);
19048
+ const bundleDir = dirname5(filePath);
18667
19049
  const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
18668
19050
  for (const node of nodes) {
18669
19051
  const nodeId = typeof node.id === "string" ? node.id : "?";
@@ -18674,12 +19056,12 @@ function runPack(options = {}) {
18674
19056
  const map2 = screenshots;
18675
19057
  for (const [platform, value] of Object.entries(map2)) {
18676
19058
  if (typeof value !== "string" || !isRelativeAssetPath(value)) continue;
18677
- const assetPath = resolve3(bundleDir, value);
18678
- if (!existsSync5(assetPath)) {
19059
+ const assetPath = resolve5(bundleDir, value);
19060
+ if (!existsSync8(assetPath)) {
18679
19061
  assetWarnings.push(`${nodeId}/${platform}: asset not found at ${assetPath} \u2014 left as-is`);
18680
19062
  continue;
18681
19063
  }
18682
- const bytes = readFileSync6(assetPath);
19064
+ const bytes = readFileSync7(assetPath);
18683
19065
  const mime = mimeForExtension(extname(assetPath));
18684
19066
  map2[platform] = `data:${mime};base64,${bytes.toString("base64")}`;
18685
19067
  inlinedAssets.push({ nodeId, platform, path: value });
@@ -18689,15 +19071,18 @@ function runPack(options = {}) {
18689
19071
  const output = serializeBundle(bundle);
18690
19072
  let outPath;
18691
19073
  if (options.out !== void 0) {
18692
- outPath = resolve3(cwd, options.out);
18693
- mkdirSync3(dirname3(outPath), { recursive: true });
18694
- writeFileSync5(outPath, output);
19074
+ outPath = resolve5(cwd, options.out);
19075
+ mkdirSync4(dirname5(outPath), { recursive: true });
19076
+ writeFileSync6(outPath, output);
18695
19077
  }
18696
- return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, output };
19078
+ return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, qualityFolded, qualityNotice, output };
18697
19079
  }
18698
19080
  function runPackCli(args) {
18699
19081
  let noJournal = false;
19082
+ let noQuality = false;
18700
19083
  let inlineAssets = false;
19084
+ let audit;
19085
+ let root;
18701
19086
  let out;
18702
19087
  const positionals = [];
18703
19088
  for (let i = 0; i < args.length; i++) {
@@ -18707,8 +19092,22 @@ function runPackCli(args) {
18707
19092
  process.exit(0);
18708
19093
  } else if (arg === "--no-journal") {
18709
19094
  noJournal = true;
19095
+ } else if (arg === "--no-quality") {
19096
+ noQuality = true;
18710
19097
  } else if (arg === "--inline-assets") {
18711
19098
  inlineAssets = true;
19099
+ } else if (arg === "--audit") {
19100
+ const value = args[++i];
19101
+ if (value === void 0) fail7(`Missing value for --audit
19102
+
19103
+ ${USAGE7}`);
19104
+ audit = value;
19105
+ } else if (arg === "--root") {
19106
+ const value = args[++i];
19107
+ if (value === void 0) fail7(`Missing value for --root
19108
+
19109
+ ${USAGE7}`);
19110
+ root = value;
18712
19111
  } else if (arg === "--out") {
18713
19112
  const value = args[++i];
18714
19113
  if (value === void 0) fail7(`Missing value for --out
@@ -18723,8 +19122,13 @@ ${USAGE7}`);
18723
19122
  positionals.push(arg);
18724
19123
  }
18725
19124
  }
18726
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH5;
18727
- const result = runPack({ path: filePath, out, noJournal, inlineAssets });
19125
+ if (noQuality && audit !== void 0) {
19126
+ fail7(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
19127
+
19128
+ ${USAGE7}`);
19129
+ }
19130
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
19131
+ const result = runPack({ path: filePath, out, noJournal, inlineAssets, noQuality, audit, root });
18728
19132
  if (!result.ok) fail7(`FATAL: ${result.fatal}`);
18729
19133
  if (result.journalIncluded) {
18730
19134
  console.error(`Journal: embedded ${result.journalEventCount} event(s)`);
@@ -18733,6 +19137,9 @@ ${USAGE7}`);
18733
19137
  } else {
18734
19138
  console.error("Journal: none to embed (no embedded journal, no sidecar)");
18735
19139
  }
19140
+ if (result.qualityNotice !== void 0) {
19141
+ console.error(result.qualityNotice);
19142
+ }
18736
19143
  for (const asset of result.inlinedAssets) {
18737
19144
  console.error(`Inlined asset: ${asset.nodeId}/${asset.platform} (${asset.path})`);
18738
19145
  }
@@ -18749,10 +19156,10 @@ ${USAGE7}`);
18749
19156
 
18750
19157
  // src/commands/open.ts
18751
19158
  import { spawn } from "node:child_process";
18752
- import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
19159
+ import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
18753
19160
  import { tmpdir } from "node:os";
18754
- import { join as join3, resolve as resolve4 } from "node:path";
18755
- var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
19161
+ import { join as join6, resolve as resolve6 } from "node:path";
19162
+ var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
18756
19163
  var OPEN_URL = "https://arkaik.app/projects";
18757
19164
  var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
18758
19165
 
@@ -18763,7 +19170,7 @@ ${OPEN_URL} (the project list's "Import JSON" picker). On an invalid bundle,
18763
19170
  findings are printed and nothing is packed, written, or opened.
18764
19171
 
18765
19172
  Arguments:
18766
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
19173
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
18767
19174
 
18768
19175
  Options:
18769
19176
  --out <path> Write the packed bundle here instead of a temp file.
@@ -18785,7 +19192,7 @@ function fatalResult3(bundlePath, message) {
18785
19192
  }
18786
19193
  async function runOpen(options = {}) {
18787
19194
  const cwd = options.cwd ?? process.cwd();
18788
- const filePath = resolve4(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
19195
+ const filePath = resolve6(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
18789
19196
  const noOpen = options.noOpen ?? false;
18790
19197
  let v;
18791
19198
  try {
@@ -18804,9 +19211,9 @@ async function runOpen(options = {}) {
18804
19211
  }
18805
19212
  let outPath = packed.outPath;
18806
19213
  if (outPath === void 0) {
18807
- const dir = mkdtempSync(join3(tmpdir(), "arkaik-open-"));
18808
- outPath = join3(dir, "bundle.json");
18809
- writeFileSync6(outPath, packed.output);
19214
+ const dir = mkdtempSync(join6(tmpdir(), "arkaik-open-"));
19215
+ outPath = join6(dir, "bundle.json");
19216
+ writeFileSync7(outPath, packed.output);
18810
19217
  }
18811
19218
  let opened = false;
18812
19219
  if (!noOpen) {
@@ -18814,7 +19221,17 @@ async function runOpen(options = {}) {
18814
19221
  await opener(OPEN_URL);
18815
19222
  opened = true;
18816
19223
  }
18817
- return { ok: true, bundlePath: filePath, valid: true, errorLines, warningLines, outPath, url: OPEN_URL, opened };
19224
+ return {
19225
+ ok: true,
19226
+ bundlePath: filePath,
19227
+ valid: true,
19228
+ errorLines,
19229
+ warningLines,
19230
+ outPath,
19231
+ url: OPEN_URL,
19232
+ opened,
19233
+ qualityNotice: packed.qualityNotice
19234
+ };
18818
19235
  }
18819
19236
  function runOpenCli(args) {
18820
19237
  let out;
@@ -18841,7 +19258,7 @@ ${USAGE8}`);
18841
19258
  positionals.push(arg);
18842
19259
  }
18843
19260
  }
18844
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
19261
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
18845
19262
  runOpen({ path: filePath, out, noOpen }).then((result) => {
18846
19263
  if (!result.ok) fail8(`FATAL: ${result.fatal}`);
18847
19264
  if (result.warningLines.length > 0) {
@@ -18854,6 +19271,9 @@ ${USAGE8}`);
18854
19271
  console.error("\nInvalid bundle \u2014 not packed, not opened.");
18855
19272
  process.exit(1);
18856
19273
  }
19274
+ if (result.qualityNotice !== void 0) {
19275
+ console.error(result.qualityNotice);
19276
+ }
18857
19277
  console.log(`Packed -> ${result.outPath}`);
18858
19278
  if (result.opened) {
18859
19279
  console.log(`Opened ${result.url}`);
@@ -18865,10 +19285,10 @@ ${USAGE8}`);
18865
19285
  }
18866
19286
 
18867
19287
  // src/commands/push.ts
18868
- import { resolve as resolve5 } from "node:path";
18869
- var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
19288
+ import { resolve as resolve7 } from "node:path";
19289
+ var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
18870
19290
  var DEFAULT_API_BASE = "https://arkaik.app";
18871
- var USAGE9 = `arkaik push [--include-journal] [--api <base-url>] [path]
19291
+ var USAGE9 = `arkaik push [--include-journal] [--include-quality] [--api <base-url>] [path]
18872
19292
  arkaik push --delete <id> --key <owner_key> [--api <base-url>]
18873
19293
 
18874
19294
  Publish a project bundle to Publik (anonymous, account-less snapshot
@@ -18882,18 +19302,29 @@ Publik-safe posture (docs/spec/journal.md). --include-journal embeds it
18882
19302
  (like a bare "arkaik pack") and forwards ?include_journal=true so the server
18883
19303
  knows to keep it.
18884
19304
 
19305
+ A Kritik "quality" section is stripped by default too, and separately:
19306
+ publishing your history and publishing your open findings are two decisions,
19307
+ not one, and both default to no. --include-quality opts in.
19308
+
18885
19309
  Snapshots are immutable: there is no update verb. Pushing again always mints
18886
19310
  a new id. The owner key printed on success is shown exactly once and cannot
18887
19311
  be recovered \u2014 save it if you may need to delete the snapshot later.
18888
19312
 
18889
19313
  Arguments:
18890
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
19314
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH8}).
18891
19315
  Ignored with --delete.
18892
19316
 
18893
19317
  Options:
18894
19318
  --include-journal Embed the journal in the pushed bundle and forward
18895
19319
  ?include_journal=true. Default: stripped, omitted
18896
19320
  entirely from the request body.
19321
+ --include-quality Embed the Kritik quality section and forward
19322
+ ?include_quality=true. Opt-in rather than opt-out
19323
+ because an open finding is an unfixed vulnerability
19324
+ plus the path to find it in (docs/rfcs/kritik.md
19325
+ \xA7 8.3) \u2014 publishing that is a decision worth typing.
19326
+ Default: deleted before packing, so it is never in the
19327
+ request body at all.
18897
19328
  --api <base-url> Publik API base URL (default: ${DEFAULT_API_BASE}).
18898
19329
  Point at a self-hosted deployment.
18899
19330
  --delete <id> Delete a snapshot by id instead of pushing. Requires
@@ -18918,8 +19349,9 @@ function fatalResult4(bundlePath, message) {
18918
19349
  }
18919
19350
  async function runPush(options = {}) {
18920
19351
  const cwd = options.cwd ?? process.cwd();
18921
- const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
19352
+ const filePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
18922
19353
  const includeJournal = options.includeJournal ?? false;
19354
+ const includeQuality = options.includeQuality ?? false;
18923
19355
  const apiBase = options.apiBase ?? DEFAULT_API_BASE;
18924
19356
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
18925
19357
  let v;
@@ -18933,11 +19365,15 @@ async function runPush(options = {}) {
18933
19365
  if (!v.valid) {
18934
19366
  return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, requestSent: false };
18935
19367
  }
18936
- const packed = runPack({ path: filePath, noJournal: !includeJournal, cwd });
19368
+ const packed = runPack({ path: filePath, noJournal: !includeJournal, noQuality: !includeQuality, cwd });
18937
19369
  if (!packed.ok) {
18938
19370
  return fatalResult4(filePath, packed.fatal ?? "pack failed");
18939
19371
  }
18940
- const url2 = `${apiBase}/api/publik${includeJournal ? "?include_journal=true" : ""}`;
19372
+ const qualityNotice = packed.qualityNotice ?? "Quality: stripped, not sent (pass --include-quality to publish it)";
19373
+ const params = [];
19374
+ if (includeJournal) params.push("include_journal=true");
19375
+ if (includeQuality) params.push("include_quality=true");
19376
+ const url2 = `${apiBase}/api/publik${params.length > 0 ? `?${params.join("&")}` : ""}`;
18941
19377
  let res;
18942
19378
  try {
18943
19379
  res = await httpClient(url2, {
@@ -18967,6 +19403,7 @@ async function runPush(options = {}) {
18967
19403
  warningLines,
18968
19404
  requestSent: true,
18969
19405
  status,
19406
+ qualityNotice,
18970
19407
  id: body.id,
18971
19408
  url: body.url,
18972
19409
  ownerKey: body.owner_key
@@ -18985,6 +19422,7 @@ async function runPush(options = {}) {
18985
19422
  warningLines,
18986
19423
  requestSent: true,
18987
19424
  status,
19425
+ qualityNotice,
18988
19426
  serverFindings: errBody.findings,
18989
19427
  retryAfter: status === 429 ? res.headers.get("retry-after") : void 0,
18990
19428
  errorMessage: errBody.message ?? `Request failed with status ${status}`
@@ -18995,6 +19433,9 @@ function reportPush(result) {
18995
19433
  console.error(`Warnings: ${result.warningLines.length}`);
18996
19434
  result.warningLines.forEach((w) => console.error(` ${w}`));
18997
19435
  }
19436
+ if (result.qualityNotice !== void 0) {
19437
+ console.error(result.qualityNotice);
19438
+ }
18998
19439
  if (!result.valid) {
18999
19440
  console.error(`Errors: ${result.errorLines.length}`);
19000
19441
  result.errorLines.forEach((e) => console.error(` ${e}`));
@@ -19076,6 +19517,7 @@ function reportDelete(id, result) {
19076
19517
  }
19077
19518
  function runPushCli(args) {
19078
19519
  let includeJournal = false;
19520
+ let includeQuality = false;
19079
19521
  let apiBase;
19080
19522
  let deleteId;
19081
19523
  let key;
@@ -19087,6 +19529,8 @@ function runPushCli(args) {
19087
19529
  process.exit(0);
19088
19530
  } else if (arg === "--include-journal") {
19089
19531
  includeJournal = true;
19532
+ } else if (arg === "--include-quality") {
19533
+ includeQuality = true;
19090
19534
  } else if (arg === "--api") {
19091
19535
  const value = args[++i];
19092
19536
  if (value === void 0) fail9(`Missing value for --api
@@ -19128,16 +19572,16 @@ ${USAGE9}`);
19128
19572
  if (key !== void 0) fail9(`--key is only valid with --delete
19129
19573
 
19130
19574
  ${USAGE9}`);
19131
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
19132
- runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
19575
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH8;
19576
+ runPush({ path: filePath, includeJournal, includeQuality, apiBase }).then((result) => {
19133
19577
  if (!result.ok) fail9(`FATAL: ${result.fatal}`);
19134
19578
  reportPush(result);
19135
19579
  }).catch((e) => fail9(`FATAL: ${e.message}`));
19136
19580
  }
19137
19581
 
19138
19582
  // src/commands/link.ts
19139
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
19140
- import { dirname as dirname4, join as join4, resolve as resolve6 } from "node:path";
19583
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
19584
+ import { dirname as dirname6, join as join7, resolve as resolve8 } from "node:path";
19141
19585
  var LINK_FILE = "docs/arkaik/arkaik.json";
19142
19586
  var DEFAULT_BASE_URL = "https://arkaik.app";
19143
19587
  var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
@@ -19208,15 +19652,15 @@ async function runLink(argv, options = {}) {
19208
19652
  return { ok: false };
19209
19653
  }
19210
19654
  const { bundle } = await res.json();
19211
- const target = resolve6(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
19212
- const linkPath = join4(target, LINK_FILE);
19213
- mkdirSync4(dirname4(linkPath), { recursive: true });
19655
+ const target = resolve8(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
19656
+ const linkPath = join7(target, LINK_FILE);
19657
+ mkdirSync5(dirname6(linkPath), { recursive: true });
19214
19658
  let existing = {};
19215
19659
  try {
19216
- existing = JSON.parse(readFileSync7(linkPath, "utf8"));
19660
+ existing = JSON.parse(readFileSync8(linkPath, "utf8"));
19217
19661
  } catch {
19218
19662
  }
19219
- writeFileSync7(
19663
+ writeFileSync8(
19220
19664
  linkPath,
19221
19665
  `${JSON.stringify({ ...existing, project_id: projectId, remote: baseUrl }, null, 2)}
19222
19666
  `
@@ -19243,14 +19687,15 @@ function runLinkCli(argv) {
19243
19687
  }
19244
19688
 
19245
19689
  // src/commands/restore.ts
19246
- import { existsSync as existsSync6, linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
19247
- import { join as join5, resolve as resolve7 } from "node:path";
19690
+ import { existsSync as existsSync9, linkSync, mkdirSync as mkdirSync6, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
19691
+ import { join as join8, resolve as resolve9 } from "node:path";
19248
19692
  var LINK_FILE2 = "docs/arkaik/arkaik.json";
19249
- var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
19693
+ var DEFAULT_BUNDLE_PATH9 = "docs/arkaik/bundle.json";
19250
19694
  var DEFAULT_API_BASE2 = "https://arkaik.app";
19251
19695
  var USAGE11 = `arkaik restore [options] [path]
19252
19696
 
19253
- Replace the linked hosted project's bundle AND journal with a local bundle \u2014
19697
+ Replace the linked hosted project's bundle, journal AND quality section with
19698
+ a local bundle \u2014
19254
19699
  the landing step for a bootstrapped map. Before sending anything, this
19255
19700
  exports the CURRENT hosted state (snapshot + journal) to
19256
19701
  docs/arkaik/.backups/<timestamp>-bundle.json (next to the link file \u2014 not
@@ -19260,9 +19705,10 @@ only way back if the restore turns out to be wrong.
19260
19705
 
19261
19706
  Arguments:
19262
19707
  path Path to the local bundle JSON file
19263
- (default: ${DEFAULT_BUNDLE_PATH8}). Its journal.jsonl
19708
+ (default: ${DEFAULT_BUNDLE_PATH9}). Its journal.jsonl
19264
19709
  sidecar (or an embedded journal, which wins) is folded
19265
- in automatically.
19710
+ in automatically, as is the docs/quality/ tree of the
19711
+ repo that bundle belongs to.
19266
19712
 
19267
19713
  Options:
19268
19714
  --dry-run Ask the server what this restore WOULD do and print
@@ -19282,6 +19728,25 @@ Options:
19282
19728
  local copy (edited in the app), not an intended
19283
19729
  deletion. Undoing a restore from a backup is the
19284
19730
  common case where it IS intended.
19731
+ --no-quality Do not send a quality section: skip the docs/quality/
19732
+ fold AND drop any section the local bundle already
19733
+ carries. Does NOT by itself permit erasing the hosted
19734
+ one \u2014 that needs --allow-quality-loss too, because
19735
+ "don't send mine" and "destroy theirs" are different
19736
+ decisions and only one of them is irreversible.
19737
+ --allow-quality-loss Proceed even though the restore would erase a quality
19738
+ section the hosted project currently has. Without this
19739
+ flag, that refuses outright \u2014 it usually means
19740
+ docs/quality/ was looked for in the wrong place, which
19741
+ --root fixes, rather than an intended wipe.
19742
+ --audit <id> Fold ONE audit's snapshot rather than merging every
19743
+ audit into current state. An id that is not on disk is
19744
+ an error, not an empty section.
19745
+ --root <dir> Where docs/quality/ lives. Default: derived from the
19746
+ bundle's own path, so restoring <repo>/docs/arkaik/
19747
+ bundle.json folds <repo>'s audits whatever directory
19748
+ you run from; only a bundle kept outside that layout
19749
+ falls back to the cwd.
19285
19750
  --api <base-url> Override the remote from docs/arkaik/arkaik.json
19286
19751
  (also overridable with $ARKAIK_URL).
19287
19752
  -h, --help Show this help.
@@ -19294,8 +19759,8 @@ function fail10(message) {
19294
19759
  console.error(message);
19295
19760
  process.exit(1);
19296
19761
  }
19297
- function fatalResult5(dryRun, message) {
19298
- return { ok: false, fatal: message, dryRun, requestSent: false };
19762
+ function fatalResult5(dryRun, message, quality = {}) {
19763
+ return { ok: false, fatal: message, dryRun, requestSent: false, ...quality };
19299
19764
  }
19300
19765
  async function safeJson(res) {
19301
19766
  try {
@@ -19321,7 +19786,9 @@ async function interpretPutResponse(res, ctx) {
19321
19786
  bundlePath: ctx.bundlePath,
19322
19787
  backupPath: ctx.backupPath,
19323
19788
  requestSent: true,
19324
- status: res.status
19789
+ status: res.status,
19790
+ qualityFolded: ctx.qualityFolded,
19791
+ qualityNotice: ctx.qualityNotice
19325
19792
  };
19326
19793
  const backupNote = backupNoteFor(ctx.backupPath);
19327
19794
  if (res.status === 200) {
@@ -19385,7 +19852,7 @@ async function interpretPutResponse(res, ctx) {
19385
19852
  }
19386
19853
  function writeBackupFile(filePath, content) {
19387
19854
  const tmpPath = `${filePath}.tmp-${process.pid}`;
19388
- writeFileSync8(tmpPath, content);
19855
+ writeFileSync9(tmpPath, content);
19389
19856
  try {
19390
19857
  linkSync(tmpPath, filePath);
19391
19858
  } finally {
@@ -19426,20 +19893,50 @@ function describeDeletions(removedNodes, removedEdges, bundlePath) {
19426
19893
  );
19427
19894
  return lines.join("\n");
19428
19895
  }
19896
+ function describeQualityLoss(hostedQuality, qualityRoot, noQuality, foldNotice) {
19897
+ const findings = Array.isArray(hostedQuality.findings) ? hostedQuality.findings.length : 0;
19898
+ const assessments = Array.isArray(hostedQuality.assessments) ? hostedQuality.assessments.length : 0;
19899
+ const parts = [];
19900
+ if (findings > 0) parts.push(`${findings} open finding${findings === 1 ? "" : "s"}`);
19901
+ if (assessments > 0) parts.push(`${assessments} assessment${assessments === 1 ? "" : "s"}`);
19902
+ const held = parts.length > 0 ? parts.join(" and ") : "no findings or assessments";
19903
+ const lines = [
19904
+ `This restore would ERASE the hosted project's quality section (${held}). Nothing was sent.`
19905
+ ];
19906
+ if (noQuality) {
19907
+ lines.push(
19908
+ `You passed --no-quality, which says "do not send my quality data" \u2014 not "destroy what is already there". Those are different decisions, and this verb has no server-side undo, so the second one has to be typed: re-run with --allow-quality-loss as well if you really mean to wipe it.`
19909
+ );
19910
+ } else {
19911
+ if (foldNotice !== void 0) {
19912
+ lines.push("The outbound bundle has no section because the fold found nothing to build one from:");
19913
+ lines.push(` ${foldNotice}`);
19914
+ }
19915
+ if (!existsSync9(join8(qualityRoot, QUALITY_DIR))) {
19916
+ lines.push(
19917
+ `There is no ${QUALITY_DIR}/ under ${qualityRoot} at all. If this project's sidecars live somewhere else, point --root at that repo and the section is rebuilt rather than removed.`
19918
+ );
19919
+ }
19920
+ lines.push(`If the hosted section really is meant to go, re-run with --allow-quality-loss.`);
19921
+ }
19922
+ return lines.join("\n");
19923
+ }
19429
19924
  async function runRestore(options = {}) {
19430
19925
  const cwd = options.cwd ?? process.cwd();
19431
19926
  const env = options.env ?? process.env;
19432
19927
  const dryRun = options.dryRun ?? false;
19433
19928
  const allowHistoryLoss = options.allowHistoryLoss ?? false;
19434
19929
  const allowDeletions = options.allowDeletions ?? false;
19930
+ const noQuality = options.noQuality ?? false;
19931
+ const allowQualityLoss = options.allowQualityLoss ?? false;
19435
19932
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
19436
- const linkPath = join5(cwd, LINK_FILE2);
19437
- if (!existsSync6(linkPath)) {
19933
+ const linkPath = join8(cwd, LINK_FILE2);
19934
+ if (!existsSync9(linkPath)) {
19438
19935
  return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
19439
19936
  }
19440
19937
  let link;
19441
19938
  try {
19442
- link = JSON.parse(readFileSync8(linkPath, "utf8"));
19939
+ link = JSON.parse(readFileSync9(linkPath, "utf8"));
19443
19940
  } catch (e) {
19444
19941
  return fatalResult5(dryRun, `Could not parse ${LINK_FILE2}: ${e.message}`);
19445
19942
  }
@@ -19449,11 +19946,11 @@ async function runRestore(options = {}) {
19449
19946
  const encodedProjectId = encodeURIComponent(projectId);
19450
19947
  const token = env.ARKAIK_TOKEN;
19451
19948
  if (!token) return fatalResult5(dryRun, `ARKAIK_TOKEN is not set. Create a token at ${baseUrl}/settings/tokens and export it.`);
19452
- const bundlePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
19453
- if (!existsSync6(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
19949
+ const bundlePath = resolve9(cwd, options.path ?? DEFAULT_BUNDLE_PATH9);
19950
+ if (!existsSync9(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
19454
19951
  let localRaw;
19455
19952
  try {
19456
- localRaw = JSON.parse(readFileSync8(bundlePath, "utf8"));
19953
+ localRaw = JSON.parse(readFileSync9(bundlePath, "utf8"));
19457
19954
  } catch (e) {
19458
19955
  return fatalResult5(dryRun, `Could not parse ${bundlePath}: ${e.message}`);
19459
19956
  }
@@ -19463,6 +19960,22 @@ async function runRestore(options = {}) {
19463
19960
  const local = localRaw;
19464
19961
  const journalEvents = loadJournalEvents(local, bundlePath);
19465
19962
  const outboundBundle = { ...local, journal: journalEvents };
19963
+ const qualityRoot = resolveQualityRoot(cwd, bundlePath, options.root);
19964
+ let qualityFolded = false;
19965
+ let qualityNotice;
19966
+ if (noQuality) {
19967
+ delete outboundBundle.quality;
19968
+ qualityNotice = "Quality: deleted before sending (--no-quality)";
19969
+ } else {
19970
+ try {
19971
+ const fold = foldQualitySection(outboundBundle, qualityRoot, options.audit);
19972
+ qualityFolded = fold.folded;
19973
+ qualityNotice = fold.notice;
19974
+ } catch (e) {
19975
+ return fatalResult5(dryRun, e.message);
19976
+ }
19977
+ }
19978
+ const qualityFields = { qualityFolded, qualityNotice };
19466
19979
  const headers = { Authorization: `Bearer ${token}` };
19467
19980
  let version2;
19468
19981
  try {
@@ -19491,7 +20004,7 @@ async function runRestore(options = {}) {
19491
20004
  } catch (e) {
19492
20005
  return { ok: true, dryRun, bundlePath, requestSent: false, errorMessage: `Network error: ${e.message}` };
19493
20006
  }
19494
- return interpretPutResponse(res2, { dryRun, bundlePath, version: version2 });
20007
+ return interpretPutResponse(res2, { dryRun, bundlePath, version: version2, ...qualityFields });
19495
20008
  }
19496
20009
  let exported;
19497
20010
  try {
@@ -19518,28 +20031,34 @@ async function runRestore(options = {}) {
19518
20031
  if (journalEvents.length < hostedEventCount && !allowHistoryLoss) {
19519
20032
  return fatalResult5(
19520
20033
  dryRun,
19521
- `This restore would replace ${hostedEventCount} hosted journal events with ${journalEvents.length}. Nothing was sent. If that is intended, re-run with --allow-history-loss; otherwise check that ${journalPathFor(bundlePath)} exists and is current.`
20034
+ `This restore would replace ${hostedEventCount} hosted journal events with ${journalEvents.length}. Nothing was sent. If that is intended, re-run with --allow-history-loss; otherwise check that ${journalPathFor(bundlePath)} exists and is current.`,
20035
+ qualityFields
19522
20036
  );
19523
20037
  }
20038
+ const hostedQuality = exportedBundle.quality;
20039
+ if (isQualitySection(hostedQuality) && !isQualitySection(outboundBundle.quality) && !allowQualityLoss) {
20040
+ return fatalResult5(dryRun, describeQualityLoss(hostedQuality, qualityRoot, noQuality, qualityNotice), qualityFields);
20041
+ }
19524
20042
  const removedNodes = removedIds(exportedBundle.nodes, Array.isArray(local.nodes) ? local.nodes : []);
19525
20043
  const removedEdges = removedIds(exportedBundle.edges, Array.isArray(local.edges) ? local.edges : []);
19526
20044
  if ((removedNodes.length > 0 || removedEdges.length > 0) && !allowDeletions) {
19527
- return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath));
20045
+ return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath), qualityFields);
19528
20046
  }
19529
- const backupDir = join5(cwd, "docs", "arkaik", ".backups");
20047
+ const backupDir = join8(cwd, "docs", "arkaik", ".backups");
19530
20048
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19531
- const backupPath = join5(backupDir, `${stamp}-bundle.json`);
20049
+ const backupPath = join8(backupDir, `${stamp}-bundle.json`);
19532
20050
  const backupContent = `${JSON.stringify(exported, null, 2)}
19533
20051
  `;
19534
20052
  try {
19535
- mkdirSync5(backupDir, { recursive: true });
20053
+ mkdirSync6(backupDir, { recursive: true });
19536
20054
  writeBackupFile(backupPath, backupContent);
19537
- JSON.parse(readFileSync8(backupPath, "utf8"));
20055
+ JSON.parse(readFileSync9(backupPath, "utf8"));
19538
20056
  } catch (e) {
19539
20057
  return fatalResult5(
19540
20058
  dryRun,
19541
20059
  `Could not write the pre-restore backup to ${backupPath}: ${e.message}
19542
- Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`
20060
+ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`,
20061
+ qualityFields
19543
20062
  );
19544
20063
  }
19545
20064
  let res;
@@ -19559,7 +20078,7 @@ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND
19559
20078
  errorMessage: `Network error: ${e.message}. Nothing was sent.${backupNoteFor(backupPath)}`
19560
20079
  };
19561
20080
  }
19562
- return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2 });
20081
+ return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2, ...qualityFields });
19563
20082
  }
19564
20083
  function printDelta(delta) {
19565
20084
  if (!delta) return;
@@ -19590,6 +20109,9 @@ function reportRestore(result) {
19590
20109
  if (result.backupPath) {
19591
20110
  console.log(`Backed up the current hosted project (snapshot + journal) to ${result.backupPath}`);
19592
20111
  }
20112
+ if (result.qualityNotice !== void 0) {
20113
+ console.log(result.qualityNotice);
20114
+ }
19593
20115
  if (!result.requestSent) {
19594
20116
  console.error(result.errorMessage ?? "Restore failed before a request could be sent.");
19595
20117
  process.exit(1);
@@ -19598,7 +20120,9 @@ function reportRestore(result) {
19598
20120
  if (result.dryRun) {
19599
20121
  console.log("[dry-run] server preview \u2014 nothing was written:");
19600
20122
  printDelta(result.delta);
19601
- console.log("Re-run without --dry-run to apply \u2014 that run takes the backup.");
20123
+ console.log(
20124
+ "Re-run without --dry-run to apply \u2014 that run takes the backup, and only that run checks the history, deletion and quality-loss guards (all three read the export, which a dry run never fetches)."
20125
+ );
19602
20126
  } else {
19603
20127
  console.log(`Restored. New version ${result.version}.`);
19604
20128
  printDelta(result.delta);
@@ -19619,6 +20143,10 @@ function runRestoreCli(argv) {
19619
20143
  let dryRun = false;
19620
20144
  let allowHistoryLoss = false;
19621
20145
  let allowDeletions = false;
20146
+ let noQuality = false;
20147
+ let allowQualityLoss = false;
20148
+ let audit;
20149
+ let root;
19622
20150
  let apiBase;
19623
20151
  const positionals = [];
19624
20152
  for (let i = 0; i < argv.length; i++) {
@@ -19633,6 +20161,22 @@ function runRestoreCli(argv) {
19633
20161
  allowHistoryLoss = true;
19634
20162
  } else if (arg === "--allow-deletions") {
19635
20163
  allowDeletions = true;
20164
+ } else if (arg === "--no-quality") {
20165
+ noQuality = true;
20166
+ } else if (arg === "--allow-quality-loss") {
20167
+ allowQualityLoss = true;
20168
+ } else if (arg === "--audit") {
20169
+ const value = argv[++i];
20170
+ if (value === void 0) fail10(`Missing value for --audit
20171
+
20172
+ ${USAGE11}`);
20173
+ audit = value;
20174
+ } else if (arg === "--root") {
20175
+ const value = argv[++i];
20176
+ if (value === void 0) fail10(`Missing value for --root
20177
+
20178
+ ${USAGE11}`);
20179
+ root = value;
19636
20180
  } else if (arg === "--api") {
19637
20181
  const value = argv[++i];
19638
20182
  if (value === void 0) fail10(`Missing value for --api
@@ -19650,28 +20194,33 @@ ${USAGE11}`);
19650
20194
  if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
19651
20195
 
19652
20196
  ${USAGE11}`);
19653
- runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
20197
+ if (noQuality && audit !== void 0) {
20198
+ fail10(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
20199
+
20200
+ ${USAGE11}`);
20201
+ }
20202
+ runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, noQuality, allowQualityLoss, audit, root, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
19654
20203
  }
19655
20204
 
19656
20205
  // src/commands/bootstrap.ts
19657
20206
  import { spawnSync as spawnSync2 } from "node:child_process";
19658
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, renameSync, writeFileSync as writeFileSync12 } from "node:fs";
20207
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync, writeFileSync as writeFileSync13 } from "node:fs";
19659
20208
  import path5 from "node:path";
19660
20209
 
19661
20210
  // src/lib/bootstrap/corpus.ts
19662
20211
  import { spawnSync } from "node:child_process";
19663
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
20212
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
19664
20213
  import path2 from "node:path";
19665
20214
 
19666
20215
  // src/lib/bootstrap/paths.ts
19667
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
20216
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
19668
20217
  import path from "node:path";
19669
20218
  var BOOTSTRAP_ROOT = ".arkaik";
19670
20219
  var CORPUS_DIR = ".arkaik/corpus";
19671
20220
  var PLAN_DIR = ".arkaik/bootstrap";
19672
20221
  var FRAGMENTS_DIR = ".arkaik/bootstrap/fragments";
19673
20222
  var MANIFEST_FILE = ".arkaik/bootstrap/manifest.json";
19674
- var PROFILE_FILE = ".arkaik/bootstrap/profile.json";
20223
+ var PROFILE_FILE2 = ".arkaik/bootstrap/profile.json";
19675
20224
  var PRS_FILE = ".arkaik/corpus/prs.jsonl";
19676
20225
  var DOCS_FILE = ".arkaik/corpus/docs.json";
19677
20226
  var SURFACES_FILE = ".arkaik/corpus/surfaces.json";
@@ -19679,16 +20228,16 @@ function at(cwd, relative) {
19679
20228
  return path.join(cwd, relative);
19680
20229
  }
19681
20230
  function ensureDir(dirPath) {
19682
- mkdirSync6(dirPath, { recursive: true });
20231
+ mkdirSync7(dirPath, { recursive: true });
19683
20232
  }
19684
20233
  function ensureGitignored(cwd) {
19685
20234
  const file2 = path.join(cwd, ".gitignore");
19686
20235
  const line2 = `${BOOTSTRAP_ROOT}/`;
19687
- const current = existsSync7(file2) ? readFileSync9(file2, "utf8") : "";
20236
+ const current = existsSync10(file2) ? readFileSync10(file2, "utf8") : "";
19688
20237
  const ignored = current.split("\n").map((l) => l.trim()).some((l) => l === line2 || l === BOOTSTRAP_ROOT);
19689
20238
  if (ignored) return false;
19690
20239
  const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
19691
- writeFileSync9(file2, `${current}${prefix}${line2}
20240
+ writeFileSync10(file2, `${current}${prefix}${line2}
19692
20241
  `);
19693
20242
  return true;
19694
20243
  }
@@ -19785,7 +20334,7 @@ function fetchPrsViaGit(cwd) {
19785
20334
  function walk(root, cwd, out) {
19786
20335
  let entries;
19787
20336
  try {
19788
- entries = readdirSync2(root, { withFileTypes: true });
20337
+ entries = readdirSync3(root, { withFileTypes: true });
19789
20338
  } catch {
19790
20339
  return;
19791
20340
  }
@@ -19803,7 +20352,7 @@ function listFiles(cwd) {
19803
20352
  }
19804
20353
  function buildDocsManifest(cwd, files) {
19805
20354
  return files.filter((f) => f.startsWith("docs/") && f.endsWith(".md")).map((f) => {
19806
- const text = readFileSync10(path2.join(cwd, f), "utf8");
20355
+ const text = readFileSync11(path2.join(cwd, f), "utf8");
19807
20356
  const heading = /^#\s+(.+)$/m.exec(text);
19808
20357
  return { path: f, title: heading ? heading[1].trim() : path2.basename(f, ".md") };
19809
20358
  });
@@ -19818,7 +20367,7 @@ function buildSurfaceInventory(files) {
19818
20367
  }
19819
20368
  function buildCorpus(options) {
19820
20369
  const { cwd } = options;
19821
- const raw = options.fromJson ? JSON.parse(readFileSync10(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
20370
+ const raw = options.fromJson ? JSON.parse(readFileSync11(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
19822
20371
  let prs = normalizePrs(raw);
19823
20372
  let sinceDroppedUndated = 0;
19824
20373
  if (options.since) {
@@ -19839,21 +20388,21 @@ function buildCorpus(options) {
19839
20388
  const docs = buildDocsManifest(cwd, files);
19840
20389
  const surfaces = buildSurfaceInventory(files);
19841
20390
  ensureDir(at(cwd, CORPUS_DIR));
19842
- writeFileSync10(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
19843
- writeFileSync10(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
20391
+ writeFileSync11(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
20392
+ writeFileSync11(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
19844
20393
  `);
19845
- writeFileSync10(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
20394
+ writeFileSync11(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
19846
20395
  `);
19847
20396
  return { prs: prs.length, docs: docs.length, surfaces: surfaces.length, sinceDroppedUndated };
19848
20397
  }
19849
20398
  function readCorpusPrs(cwd) {
19850
20399
  const file2 = at(cwd, PRS_FILE);
19851
- if (!existsSync8(file2)) return [];
19852
- return readFileSync10(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
20400
+ if (!existsSync11(file2)) return [];
20401
+ return readFileSync11(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
19853
20402
  }
19854
20403
 
19855
20404
  // src/lib/bootstrap/fragments.ts
19856
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
20405
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
19857
20406
  import path3 from "node:path";
19858
20407
  function isArrayOfObjects(value) {
19859
20408
  return value === void 0 || Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null && !Array.isArray(v));
@@ -19879,13 +20428,13 @@ function loadFragments(cwd, manifest) {
19879
20428
  problems.push({ unit: String(unit2.id), message: err instanceof Error ? err.message : String(err) });
19880
20429
  continue;
19881
20430
  }
19882
- if (!existsSync9(file2)) {
20431
+ if (!existsSync12(file2)) {
19883
20432
  missing.push(unit2.id);
19884
20433
  continue;
19885
20434
  }
19886
20435
  let parsed;
19887
20436
  try {
19888
- parsed = JSON.parse(readFileSync11(file2, "utf8"));
20437
+ parsed = JSON.parse(readFileSync12(file2, "utf8"));
19889
20438
  } catch (err) {
19890
20439
  problems.push({ unit: unit2.id, message: `not valid JSON: ${err instanceof Error ? err.message : "parse error"}` });
19891
20440
  continue;
@@ -19922,7 +20471,7 @@ function renderIndex(bundle) {
19922
20471
  }
19923
20472
 
19924
20473
  // src/lib/bootstrap/manifest.ts
19925
- import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "node:fs";
20474
+ import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "node:fs";
19926
20475
  import path4 from "node:path";
19927
20476
 
19928
20477
  // src/lib/bootstrap/era-window.ts
@@ -20030,19 +20579,19 @@ function assertValidProfile(profile) {
20030
20579
 
20031
20580
  // src/lib/bootstrap/manifest.ts
20032
20581
  function readProfile(cwd) {
20033
- const file2 = at(cwd, PROFILE_FILE);
20034
- if (!existsSync10(file2)) return null;
20582
+ const file2 = at(cwd, PROFILE_FILE2);
20583
+ if (!existsSync13(file2)) return null;
20035
20584
  try {
20036
- return JSON.parse(readFileSync12(file2, "utf8"));
20585
+ return JSON.parse(readFileSync13(file2, "utf8"));
20037
20586
  } catch (err) {
20038
- throw new Error(`cannot read ${PROFILE_FILE}: ${err instanceof Error ? err.message : String(err)}`);
20587
+ throw new Error(`cannot read ${PROFILE_FILE2}: ${err instanceof Error ? err.message : String(err)}`);
20039
20588
  }
20040
20589
  }
20041
20590
  function readManifest(cwd) {
20042
20591
  const file2 = at(cwd, MANIFEST_FILE);
20043
- if (!existsSync10(file2)) return null;
20592
+ if (!existsSync13(file2)) return null;
20044
20593
  try {
20045
- return JSON.parse(readFileSync12(file2, "utf8"));
20594
+ return JSON.parse(readFileSync13(file2, "utf8"));
20046
20595
  } catch (err) {
20047
20596
  throw new Error(`cannot read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
20048
20597
  }
@@ -20050,15 +20599,15 @@ function readManifest(cwd) {
20050
20599
  function writeManifest(cwd, manifest) {
20051
20600
  ensureDir(at(cwd, PLAN_DIR));
20052
20601
  ensureDir(at(cwd, FRAGMENTS_DIR));
20053
- writeFileSync11(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
20602
+ writeFileSync12(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
20054
20603
  `);
20055
20604
  }
20056
20605
  function detectMode(cwd, bundlePath) {
20057
20606
  const file2 = path4.resolve(cwd, bundlePath);
20058
- if (!existsSync10(file2)) return "greenfield";
20607
+ if (!existsSync13(file2)) return "greenfield";
20059
20608
  let parsed;
20060
20609
  try {
20061
- parsed = JSON.parse(readFileSync12(file2, "utf8"));
20610
+ parsed = JSON.parse(readFileSync13(file2, "utf8"));
20062
20611
  } catch (err) {
20063
20612
  throw new Error(`cannot read bundle at ${bundlePath}: ${err instanceof Error ? err.message : String(err)}`);
20064
20613
  }
@@ -20511,7 +21060,7 @@ function mergeFragments(input) {
20511
21060
  }
20512
21061
 
20513
21062
  // src/lib/bootstrap/slice.ts
20514
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
21063
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
20515
21064
 
20516
21065
  // src/lib/bootstrap/body-budget.ts
20517
21066
  var LAB_NOTE_HEADING_RE = /^##\s+Lab Note.*$/m;
@@ -20553,8 +21102,8 @@ function boundBody(body) {
20553
21102
 
20554
21103
  // src/lib/bootstrap/slice.ts
20555
21104
  function readJsonArray(file2) {
20556
- if (!existsSync11(file2)) return [];
20557
- const parsed = JSON.parse(readFileSync13(file2, "utf8"));
21105
+ if (!existsSync14(file2)) return [];
21106
+ const parsed = JSON.parse(readFileSync14(file2, "utf8"));
20558
21107
  return Array.isArray(parsed) ? parsed : [];
20559
21108
  }
20560
21109
  function toPosix(value) {
@@ -20575,7 +21124,7 @@ function eraWindows(cwd, slugs) {
20575
21124
  const era = bySlug.get(slug);
20576
21125
  if (!era) {
20577
21126
  throw new Error(
20578
- `era "${slug}" is not declared in ${PROFILE_FILE}'s "eras" list, but a work unit's slice references it. profile.json may have been edited (or the era removed) since \`arkaik bootstrap plan\` last ran. Restore the era in profile.json and re-run \`arkaik bootstrap plan\`, or reconcile the manifest by hand.`
21127
+ `era "${slug}" is not declared in ${PROFILE_FILE2}'s "eras" list, but a work unit's slice references it. profile.json may have been edited (or the era removed) since \`arkaik bootstrap plan\` last ran. Restore the era in profile.json and re-run \`arkaik bootstrap plan\`, or reconcile the manifest by hand.`
20579
21128
  );
20580
21129
  }
20581
21130
  assertEraWindow(era);
@@ -20664,7 +21213,7 @@ ${usage}`);
20664
21213
  }
20665
21214
  function writeFileAtomic(filePath, content) {
20666
21215
  const tmpPath = `${filePath}.tmp-${process.pid}`;
20667
- writeFileSync12(tmpPath, content);
21216
+ writeFileSync13(tmpPath, content);
20668
21217
  renameSync(tmpPath, filePath);
20669
21218
  }
20670
21219
  function runCorpus(argv) {
@@ -20699,7 +21248,7 @@ ${CORPUS_USAGE}`);
20699
21248
  ${CORPUS_USAGE}`);
20700
21249
  }
20701
21250
  }
20702
- if (!existsSync12(path5.join(cwd, ".git"))) {
21251
+ if (!existsSync15(path5.join(cwd, ".git"))) {
20703
21252
  fail11("`arkaik bootstrap corpus` must run from the repository root (no .git here).");
20704
21253
  }
20705
21254
  try {
@@ -20761,7 +21310,7 @@ ${PLAN_USAGE}`);
20761
21310
 
20762
21311
  ${PLAN_USAGE}`);
20763
21312
  }
20764
- if (!existsSync12(path5.join(cwd, ".git"))) {
21313
+ if (!existsSync15(path5.join(cwd, ".git"))) {
20765
21314
  fail11("`arkaik bootstrap plan` must run from the repository root (no .git here).");
20766
21315
  }
20767
21316
  try {
@@ -20900,7 +21449,7 @@ ${MERGE_USAGE}`);
20900
21449
  }
20901
21450
  try {
20902
21451
  const bundlePath = path5.resolve(cwd, manifest.bundle);
20903
- const base = existsSync12(bundlePath) ? readBundle(bundlePath) : {
21452
+ const base = existsSync15(bundlePath) ? readBundle(bundlePath) : {
20904
21453
  schema_version: 3,
20905
21454
  project: {
20906
21455
  id: path5.basename(cwd),
@@ -20936,7 +21485,7 @@ ${MERGE_USAGE}`);
20936
21485
  const journalPath = journalPathFor(bundlePath);
20937
21486
  const journalText = result.journal.map((e) => JSON.stringify(e)).join("\n") + (result.journal.length ? "\n" : "");
20938
21487
  if (!dryRun) {
20939
- mkdirSync7(path5.dirname(bundlePath), { recursive: true });
21488
+ mkdirSync8(path5.dirname(bundlePath), { recursive: true });
20940
21489
  writeFileAtomic(bundlePath, serialized);
20941
21490
  writeFileAtomic(journalPath, journalText);
20942
21491
  }
@@ -20985,138 +21534,6 @@ ${USAGE12}`);
20985
21534
  }
20986
21535
  }
20987
21536
 
20988
- // ../schema/src/cli/kritik-audit.ts
20989
- import { existsSync as existsSync14, readdirSync as readdirSync3, statSync } from "node:fs";
20990
- import { join as join7 } from "node:path";
20991
-
20992
- // ../schema/src/cli/kritik-paths.ts
20993
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "node:fs";
20994
- import { dirname as dirname5, join as join6, resolve as resolve8 } from "node:path";
20995
- var QUALITY_DIR = "docs/quality";
20996
- var PROFILE_FILE2 = "profile.json";
20997
- var OVERLAY_FILE = "criteria.custom.json";
20998
- var AUDITS_DIR = "audits";
20999
- var PACK_FILE = "library.json";
21000
- function readJson(path6) {
21001
- return JSON.parse(readFileSync14(path6, "utf8"));
21002
- }
21003
- function writeJson(path6, value) {
21004
- mkdirSync8(dirname5(path6), { recursive: true });
21005
- writeFileSync13(path6, JSON.stringify(value, null, 2) + "\n");
21006
- }
21007
- var profilePath = (root) => join6(root, QUALITY_DIR, PROFILE_FILE2);
21008
- var overlayPath = (root) => join6(root, QUALITY_DIR, OVERLAY_FILE);
21009
- var auditDir = (root, auditId) => join6(root, QUALITY_DIR, AUDITS_DIR, auditId);
21010
- function loadProfile(root) {
21011
- const path6 = profilePath(root);
21012
- return existsSync13(path6) ? readJson(path6) : null;
21013
- }
21014
- function loadOverlay(root) {
21015
- const path6 = overlayPath(root);
21016
- return existsSync13(path6) ? readJson(path6) : null;
21017
- }
21018
-
21019
- // ../schema/src/cli/kritik-audit.ts
21020
- var SCORES_FILE = "scores.json";
21021
- var FINDINGS_FILE = "findings.json";
21022
- var MATRIX_FILE = "matrix.json";
21023
- var scoresPath = (root, auditId) => join7(auditDir(root, auditId), SCORES_FILE);
21024
- var findingsPath = (root, auditId) => join7(auditDir(root, auditId), FINDINGS_FILE);
21025
- var matrixPath = (root, auditId) => join7(auditDir(root, auditId), MATRIX_FILE);
21026
- function listAuditIds(root) {
21027
- const dir = join7(root, QUALITY_DIR, AUDITS_DIR);
21028
- if (!existsSync14(dir)) return [];
21029
- return readdirSync3(dir).filter((name) => statSync(join7(dir, name)).isDirectory()).sort();
21030
- }
21031
- function newestAuditId(root) {
21032
- const dir = join7(root, QUALITY_DIR, AUDITS_DIR);
21033
- if (!existsSync14(dir)) throw new Error(`no audits directory at ${dir}`);
21034
- const ids = listAuditIds(root);
21035
- if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
21036
- return ids[ids.length - 1];
21037
- }
21038
- function loadScores(root, auditId) {
21039
- const path6 = scoresPath(root, auditId);
21040
- if (!existsSync14(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
21041
- const file2 = readJson(path6);
21042
- return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
21043
- }
21044
- function loadScoresOrEmpty(root, auditId) {
21045
- return existsSync14(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
21046
- }
21047
- function loadFindings(root, auditId) {
21048
- const path6 = findingsPath(root, auditId);
21049
- if (!existsSync14(path6)) return { audit_id: auditId, findings: [] };
21050
- const file2 = readJson(path6);
21051
- return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
21052
- }
21053
- function saveScores(root, auditId, file2) {
21054
- writeJson(scoresPath(root, auditId), file2);
21055
- }
21056
- function saveFindings(root, auditId, file2) {
21057
- writeJson(findingsPath(root, auditId), file2);
21058
- }
21059
- function requireProfile(root) {
21060
- const profile = loadProfile(root);
21061
- if (!profile) {
21062
- throw new Error(
21063
- `no profile at ${join7(root, QUALITY_DIR, "profile.json")} \u2014 pick this project's surfaces first (\`arkaik kritik profile\`, or the plugin's init-profile.js).`
21064
- );
21065
- }
21066
- return profile;
21067
- }
21068
- function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
21069
- const findings = loadFindings(root, auditId);
21070
- return {
21071
- framework_version: scores.framework_version ?? library.version,
21072
- profile: requireProfile(root),
21073
- assessments: scores.assessments,
21074
- findings: findings.findings
21075
- };
21076
- }
21077
- function computeAuditMatrix(root, auditId, library) {
21078
- const scores = loadScores(root, auditId);
21079
- const section = loadQualitySection(root, auditId, library, scores);
21080
- const matrix = deriveQualityMatrix({ quality: section }, library);
21081
- const file2 = {
21082
- audit_id: auditId,
21083
- commit: scores.commit,
21084
- framework_version: section.framework_version,
21085
- matrix: matrix.matrix,
21086
- overall: matrix.overall,
21087
- finding_counts: matrix.finding_counts
21088
- };
21089
- writeJson(matrixPath(root, auditId), file2);
21090
- return { section, matrix, file: file2 };
21091
- }
21092
- function renderMatrixMarkdown(matrix, domainNames) {
21093
- const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
21094
- const lines = [];
21095
- lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
21096
- lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
21097
- for (const domain2 of matrix.domains) {
21098
- const label = domainNames.get(domain2) ?? domain2;
21099
- lines.push(
21100
- `| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
21101
- );
21102
- }
21103
- lines.push(
21104
- `| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
21105
- const score = matrix.overall[s];
21106
- return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
21107
- }).join(" | ")} |`
21108
- );
21109
- return lines.join("\n");
21110
- }
21111
- function locateFinding(root, id) {
21112
- for (const auditId of [...listAuditIds(root)].reverse()) {
21113
- const file2 = loadFindings(root, auditId);
21114
- const finding = file2.findings.find((candidate) => candidate.id === id);
21115
- if (finding) return { auditId, file: file2, finding };
21116
- }
21117
- return void 0;
21118
- }
21119
-
21120
21537
  // ../schema/src/cli/kritik-overlay.ts
21121
21538
  var ANCHOR_KEYS = ["l0", "l1", "l2", "l3", "l4"];
21122
21539
  var CRITERION_TEMPLATE = {
@@ -21247,49 +21664,6 @@ Pass --domain-name "<display name>" to define it, or use an existing domain code
21247
21664
 
21248
21665
  // src/commands/kritik.ts
21249
21666
  import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
21250
-
21251
- // src/lib/kritik-io.ts
21252
- import { existsSync as existsSync15 } from "node:fs";
21253
- import { dirname as dirname6, join as join8 } from "node:path";
21254
- import { fileURLToPath as fileURLToPath2 } from "node:url";
21255
- var KRITIK_ACTOR = "arkaik-cli";
21256
- var DEFAULT_BUNDLE_PATH9 = join8("docs", "arkaik", "bundle.json");
21257
- var VENDORED_PACK = join8(QUALITY_DIR, PACK_FILE);
21258
- var BUNDLED_PACK = join8(dirname6(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
21259
- function resolvePack(root) {
21260
- const vendored = join8(root, VENDORED_PACK);
21261
- if (existsSync15(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
21262
- if (!existsSync15(BUNDLED_PACK)) {
21263
- throw new Error(
21264
- `no criteria pack found. Looked in:
21265
- ${vendored}
21266
- ${BUNDLED_PACK}
21267
- The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
21268
- );
21269
- }
21270
- return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
21271
- }
21272
- function loadKritikLibrary(root) {
21273
- const pack = resolvePack(root);
21274
- return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
21275
- }
21276
- function resolveJournal(root, bundlePath) {
21277
- const resolved = bundlePath ?? join8(root, DEFAULT_BUNDLE_PATH9);
21278
- return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync15(resolved) };
21279
- }
21280
- function appendQualityEvents(root, inputs, options = {}) {
21281
- if (inputs.length === 0) return { events: [] };
21282
- const actor = options.actor ?? KRITIK_ACTOR;
21283
- const journal = resolveJournal(root, options.bundlePath);
21284
- if (!journal.present) return { events: [] };
21285
- const bundle = readBundle(journal.bundlePath);
21286
- const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
21287
- const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
21288
- for (const event of events) appendJournalEvent(journal.journalPath, event);
21289
- return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
21290
- }
21291
-
21292
- // src/commands/kritik.ts
21293
21667
  var USAGE13 = `arkaik kritik <subcommand> [options]
21294
21668
 
21295
21669
  Audit this product's quality with the Kritik framework: a maturity level per
@@ -21304,6 +21678,7 @@ Subcommands:
21304
21678
  finding accept <id> Accept it as a known, owned risk.
21305
21679
  matrix [audit-id] Roll an audit up (writes matrix.json).
21306
21680
  signals The signal pack, and what has tripped since the last audit.
21681
+ regressions What got worse between two audits.
21307
21682
  issue <criterion> Print the prefilled GitHub issue skeleton.
21308
21683
  criterion add ... Add a project-specific criterion to the overlay.
21309
21684
 
@@ -21377,6 +21752,20 @@ makes it usable as a CI step.
21377
21752
  --trip Record one as tripped: appends quality.signal.tripped.
21378
21753
  --signal takes the row's index from the run sheet, or the text.
21379
21754
  --json The full run sheet as JSON (what an agent should read).`;
21755
+ var REGRESSIONS_USAGE = `arkaik kritik regressions [--from <audit>] [--to <audit>] [--record] [--json]
21756
+
21757
+ What got worse between two audits: a cell whose maturity dropped, a cell that
21758
+ gained an open Critical or High finding, a finding that was resolved and is open
21759
+ again. Cells scored in only one of the two audits are not compared \u2014 a
21760
+ half-finished audit is not a regression.
21761
+
21762
+ Exits 1 when anything regressed, which is what makes it usable as a CI step or a
21763
+ scheduled routine.
21764
+
21765
+ --from <audit> The older reading (default: the audit before --to).
21766
+ --to <audit> The newer reading (default: the newest on disk).
21767
+ --record Append one quality.signal.tripped per regression.
21768
+ --json The full list as JSON.`;
21380
21769
  var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
21381
21770
 
21382
21771
  Print the criterion's GitHub issue skeleton, filled as far as what we know
@@ -21930,7 +22319,8 @@ function runSignals(args, common) {
21930
22319
  console.log(
21931
22320
  `
21932
22321
  ${rows.length} checks across ${criteria} criteria and ${(profile.surfaces ?? []).length} surfaces.
21933
- Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.`
22322
+ Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.` + (listAuditIds(common.root).length > 1 ? `
22323
+ Comparing two audits is \`arkaik kritik regressions\`.` : "")
21934
22324
  );
21935
22325
  }
21936
22326
  if (trips.length > 0) {
@@ -21946,6 +22336,83 @@ function runSignals(args, common) {
21946
22336
  console.log("");
21947
22337
  process.exit(0);
21948
22338
  }
22339
+ function auditPair(root, from, to) {
22340
+ const audits = listAuditIds(root);
22341
+ if (audits.length < 2) {
22342
+ fail12(
22343
+ `kritik: regressions needs two audits to compare \u2014 ${audits.length === 0 ? "docs/quality/audits/ holds none" : `only "${audits[0]}" exists`}.
22344
+ A regression is the difference between two readings; one reading is a baseline.`
22345
+ );
22346
+ }
22347
+ const known = (id) => {
22348
+ if (!audits.includes(id)) fail12(`kritik: no audit "${id}" under docs/quality/audits/ (have: ${audits.join(", ")})`);
22349
+ return id;
22350
+ };
22351
+ const newer = to === void 0 ? audits[audits.length - 1] : known(to);
22352
+ const older = from === void 0 ? audits[audits.indexOf(newer) - 1] : known(from);
22353
+ if (older === void 0) {
22354
+ fail12(`kritik: "${newer}" is the oldest audit \u2014 there is nothing before it to compare against.`);
22355
+ }
22356
+ if (older === newer) {
22357
+ fail12(`kritik: --from and --to name the same audit ("${newer}") \u2014 a regression needs two readings.`);
22358
+ }
22359
+ if (audits.indexOf(older) > audits.indexOf(newer)) {
22360
+ fail12(`kritik: --from "${older}" is newer than --to "${newer}" \u2014 swap them, or the comparison inverts.`);
22361
+ }
22362
+ return { from: older, to: newer };
22363
+ }
22364
+ function runRegressions(args, common) {
22365
+ const { single, flags } = collect(args, [], ["json", "record"]);
22366
+ if (flags.has("help")) {
22367
+ console.log(REGRESSIONS_USAGE);
22368
+ process.exit(0);
22369
+ }
22370
+ const library = loadLibraryOrFail(common.root);
22371
+ profileOrFail(common.root);
22372
+ const { from, to } = auditPair(common.root, single.from, single.to);
22373
+ let regressions;
22374
+ try {
22375
+ regressions = detectRegressions(
22376
+ loadQualitySection(common.root, from, library),
22377
+ loadQualitySection(common.root, to, library),
22378
+ library
22379
+ );
22380
+ } catch (error51) {
22381
+ return fail12(`kritik: ${error51.message}`);
22382
+ }
22383
+ if (flags.has("json")) {
22384
+ console.log(JSON.stringify({ from, to, total: regressions.length, regressions }, null, 2));
22385
+ } else if (regressions.length === 0) {
22386
+ console.log(`
22387
+ nothing regressed between ${from} and ${to}.
22388
+ `);
22389
+ } else {
22390
+ console.log("");
22391
+ for (const regression of regressions) {
22392
+ console.log(` [${regression.kind}] ${regression.criterion_id} x ${regression.surface}`);
22393
+ console.log(` ${regression.detail}`);
22394
+ }
22395
+ console.log(`
22396
+ ${regressions.length} regression${regressions.length === 1 ? "" : "s"} between ${from} and ${to}.`);
22397
+ }
22398
+ if (flags.has("record") && regressions.length > 0) {
22399
+ reportJournal(
22400
+ common.root,
22401
+ regressions.map(
22402
+ (regression) => signalTrippedInput({
22403
+ criterion_id: regression.criterion_id,
22404
+ surface: regression.surface,
22405
+ signal: regression.signal,
22406
+ detail: regression.detail
22407
+ })
22408
+ ),
22409
+ common
22410
+ );
22411
+ console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
22412
+ `);
22413
+ }
22414
+ process.exit(regressions.length > 0 ? 1 : 0);
22415
+ }
21949
22416
  function runIssue(args, common) {
21950
22417
  const { single, flags, positionals } = collect(args);
21951
22418
  if (flags.has("help")) {
@@ -22085,6 +22552,8 @@ function runKritik(args) {
22085
22552
  return runMatrix(subArgs, common);
22086
22553
  case "signals":
22087
22554
  return runSignals(subArgs, common);
22555
+ case "regressions":
22556
+ return runRegressions(subArgs, common);
22088
22557
  case "issue":
22089
22558
  return runIssue(subArgs, common);
22090
22559
  case "criterion":
@@ -22109,13 +22578,13 @@ Commands:
22109
22578
  release <version> [path] Tag a release (append release.tagged) and draft its notes.
22110
22579
  deliverable <title> [path] Record a deliverable (append deliverable.shipped).
22111
22580
  sync [options] [path] Mirror external ref status (GitHub issues/PRs) into node refs.
22112
- pack [options] [path] Produce a single self-contained interchange bundle (embeds the journal).
22581
+ pack [options] [path] Produce a self-contained interchange bundle (embeds journal + quality).
22113
22582
  open [options] [path] Validate, then hand off the packed bundle to arkaik.app import.
22114
- push [options] [path] Validate, pack (journal stripped), and publish to Publik.
22583
+ push [options] [path] Validate, pack (journal + quality stripped), and publish to Publik.
22115
22584
  --delete <id> --key <owner_key> removes a snapshot.
22116
22585
  link [options] [path] Point this repo at a hosted project so an agent can edit it.
22117
22586
  --list shows the projects your token can reach.
22118
- restore [options] [path] Replace the linked hosted project's bundle + journal (backs up first).
22587
+ restore [options] [path] Replace the linked project's bundle, journal + quality (backs up first).
22119
22588
  bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
22120
22589
  kritik <sub> [options] Quality audits: score criteria, open findings, roll up the matrix.
22121
22590
 
@@ -22124,7 +22593,7 @@ Options:
22124
22593
  -v, --version Print the version.
22125
22594
 
22126
22595
  Run "arkaik <command> --help" for command-specific help.`;
22127
- var VERSION = "0.2.0";
22596
+ var VERSION = "0.3.0";
22128
22597
  function main(argv) {
22129
22598
  const [command, ...rest] = argv;
22130
22599
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {