arkaik 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -15216,6 +15216,13 @@ var QualityFindingResolvedEventSchema = external_exports.object({
15216
15216
  resolved_by: external_exports.string().optional(),
15217
15217
  node_ids: external_exports.array(external_exports.string()).optional()
15218
15218
  }).catchall(external_exports.unknown());
15219
+ var QualityFindingAcceptedEventSchema = external_exports.object({
15220
+ ...envelope,
15221
+ type: external_exports.literal("quality.finding.accepted"),
15222
+ finding_id: external_exports.string(),
15223
+ reason: external_exports.string(),
15224
+ node_ids: external_exports.array(external_exports.string()).optional()
15225
+ }).catchall(external_exports.unknown());
15219
15226
  var QualitySignalTrippedEventSchema = external_exports.object({
15220
15227
  ...envelope,
15221
15228
  type: external_exports.literal("quality.signal.tripped"),
@@ -15243,6 +15250,7 @@ var JOURNAL_EVENT_SCHEMAS = {
15243
15250
  "quality.audit.completed": QualityAuditCompletedEventSchema,
15244
15251
  "quality.finding.opened": QualityFindingOpenedEventSchema,
15245
15252
  "quality.finding.resolved": QualityFindingResolvedEventSchema,
15253
+ "quality.finding.accepted": QualityFindingAcceptedEventSchema,
15246
15254
  "quality.signal.tripped": QualitySignalTrippedEventSchema
15247
15255
  };
15248
15256
  var KnownJournalEventSchema = external_exports.union([
@@ -15264,6 +15272,7 @@ var KnownJournalEventSchema = external_exports.union([
15264
15272
  QualityAuditCompletedEventSchema,
15265
15273
  QualityFindingOpenedEventSchema,
15266
15274
  QualityFindingResolvedEventSchema,
15275
+ QualityFindingAcceptedEventSchema,
15267
15276
  QualitySignalTrippedEventSchema
15268
15277
  ]);
15269
15278
 
@@ -15351,6 +15360,9 @@ var QualityFindingSchema = external_exports.object({
15351
15360
  remediation: external_exports.string().optional(),
15352
15361
  node_ids: external_exports.array(external_exports.string()).optional().meta({ description: "Graph nodes this finding is about \u2014 the tie deliverable.shipped already uses." }),
15353
15362
  issue_url: external_exports.string().optional(),
15363
+ commit: external_exports.string().optional().meta({
15364
+ description: "Commit SHA anchoring the file:line evidence. Required by policy on any finding written away from a checkout (issue #400 decision 4) \u2014 without it a stale citation is indistinguishable from a wrong one."
15365
+ }),
15354
15366
  verification: external_exports.object({ verdict: external_exports.enum(["CONFIRMED", "REFUTED", "DOWNGRADED"]), note: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Result of the adversarial refutation pass. Refuted findings are disclosed, not deleted." })
15355
15367
  }).catchall(external_exports.unknown()).meta({
15356
15368
  id: "QualityFinding",
@@ -16611,6 +16623,15 @@ function validateBundle(input) {
16611
16623
  );
16612
16624
  }
16613
16625
  }
16626
+ if (type === "quality.finding.accepted" && typeof event.finding_id === "string") {
16627
+ if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
16628
+ warn(
16629
+ `journal[${index}].finding_id`,
16630
+ "quality-accepted-never-opened",
16631
+ `Journal event ${index}: accepts finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
16632
+ );
16633
+ }
16634
+ }
16614
16635
  });
16615
16636
  }
16616
16637
  for (const finding of crossCheckJournal(bundle)) findings.push(finding);
@@ -17074,11 +17095,14 @@ function resolveFinding(findings, id, resolvedBy) {
17074
17095
  ...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {}
17075
17096
  });
17076
17097
  }
17077
- function acceptFinding(findings, id, note) {
17078
- const existing = findings.find((candidate) => candidate.id === id);
17079
- const detail = existing === void 0 ? note : `${existing.detail}
17098
+ function acceptedDetail(detail, note) {
17099
+ return `${detail}
17080
17100
 
17081
17101
  Accepted risk: ${note}`.trim();
17102
+ }
17103
+ function acceptFinding(findings, id, note) {
17104
+ const existing = findings.find((candidate) => candidate.id === id);
17105
+ const detail = existing === void 0 ? note : acceptedDetail(existing.detail, note);
17082
17106
  return patchFinding(findings, id, { status: "accepted-risk", detail });
17083
17107
  }
17084
17108
  function fillTemplate(text, values) {
@@ -17210,6 +17234,83 @@ function signalTrippedInput(trip) {
17210
17234
  };
17211
17235
  }
17212
17236
 
17237
+ // ../schema/src/quality-regressions.ts
17238
+ var REGRESSION_SIGNALS = {
17239
+ "level-drop": "maturity on this cell does not regress",
17240
+ "new-severe-finding": "no open Critical or High finding on this cell",
17241
+ "reopened-finding": "a resolved finding stays resolved"
17242
+ };
17243
+ var cellKey = (criterionId, surface) => `${criterionId}::${surface}`;
17244
+ var rowsOf = (value) => Array.isArray(value) ? value : [];
17245
+ function assessmentsByCell(state) {
17246
+ const cells = /* @__PURE__ */ new Map();
17247
+ for (const assessment of rowsOf(state?.assessments)) {
17248
+ if (typeof assessment?.criterion_id !== "string" || typeof assessment?.surface !== "string") continue;
17249
+ cells.set(cellKey(assessment.criterion_id, assessment.surface), assessment);
17250
+ }
17251
+ return cells;
17252
+ }
17253
+ function severeByCell(state, library) {
17254
+ const cells = /* @__PURE__ */ new Map();
17255
+ for (const finding of rowsOf(state?.findings)) {
17256
+ if (typeof finding?.criterion_id !== "string" || typeof finding?.surface !== "string") continue;
17257
+ if (!isOpenFinding(finding)) continue;
17258
+ const severity = severityOf(finding, library);
17259
+ if (severity !== "critical" && severity !== "high") continue;
17260
+ const key = cellKey(finding.criterion_id, finding.surface);
17261
+ const existing = cells.get(key);
17262
+ if (existing === void 0) cells.set(key, [finding]);
17263
+ else existing.push(finding);
17264
+ }
17265
+ return cells;
17266
+ }
17267
+ function detectRegressions(previous, next, library) {
17268
+ const before = assessmentsByCell(previous);
17269
+ const after = assessmentsByCell(next);
17270
+ const comparable = (key) => before.has(key) && after.has(key);
17271
+ const regressions = [];
17272
+ for (const [key, current] of after) {
17273
+ const earlier = before.get(key);
17274
+ if (earlier === void 0) continue;
17275
+ if (!(Number(current.level) < Number(earlier.level))) continue;
17276
+ regressions.push({
17277
+ kind: "level-drop",
17278
+ criterion_id: current.criterion_id,
17279
+ surface: current.surface,
17280
+ signal: REGRESSION_SIGNALS["level-drop"],
17281
+ detail: `level ${earlier.level} \u2192 ${current.level} (${earlier.audit_id} \u2192 ${current.audit_id})`
17282
+ });
17283
+ }
17284
+ const severeBefore = severeByCell(previous, library);
17285
+ for (const [key, findings] of severeByCell(next, library)) {
17286
+ if (!comparable(key)) continue;
17287
+ if ((severeBefore.get(key) ?? []).length > 0) continue;
17288
+ for (const finding of findings) {
17289
+ regressions.push({
17290
+ kind: "new-severe-finding",
17291
+ criterion_id: finding.criterion_id,
17292
+ surface: finding.surface,
17293
+ signal: REGRESSION_SIGNALS["new-severe-finding"],
17294
+ detail: `${finding.id} \u2014 ${severityOf(finding, library)} (impact ${finding.impact} \xD7 likelihood ${finding.likelihood})`
17295
+ });
17296
+ }
17297
+ }
17298
+ const resolvedBefore = new Set(
17299
+ rowsOf(previous?.findings).filter((finding) => finding?.status === "resolved").map((finding) => finding.id)
17300
+ );
17301
+ for (const finding of rowsOf(next?.findings)) {
17302
+ if (!isOpenFinding(finding) || !resolvedBefore.has(finding.id)) continue;
17303
+ regressions.push({
17304
+ kind: "reopened-finding",
17305
+ criterion_id: finding.criterion_id,
17306
+ surface: finding.surface,
17307
+ signal: REGRESSION_SIGNALS["reopened-finding"],
17308
+ detail: `${finding.id} \u2014 "${finding.title}"`
17309
+ });
17310
+ }
17311
+ return regressions;
17312
+ }
17313
+
17213
17314
  // src/commands/init.ts
17214
17315
  var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
17215
17316
  var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
@@ -18576,18 +18677,290 @@ ${USAGE6}`);
18576
18677
  }
18577
18678
 
18578
18679
  // src/commands/pack.ts
18680
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
18681
+ import { dirname as dirname5, extname, resolve as resolve5 } from "node:path";
18682
+
18683
+ // src/lib/kritik-io.ts
18684
+ import { existsSync as existsSync7 } from "node:fs";
18685
+ import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
18686
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
18687
+
18688
+ // ../schema/src/cli/kritik-audit.ts
18689
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "node:fs";
18690
+ import { join as join4 } from "node:path";
18691
+
18692
+ // ../schema/src/cli/kritik-paths.ts
18579
18693
  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]
18694
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
18695
+ var QUALITY_DIR = "docs/quality";
18696
+ var PROFILE_FILE = "profile.json";
18697
+ var OVERLAY_FILE = "criteria.custom.json";
18698
+ var AUDITS_DIR = "audits";
18699
+ var PACK_FILE = "library.json";
18700
+ function readJson(path6) {
18701
+ const text = readFileSync6(path6, "utf8");
18702
+ try {
18703
+ return JSON.parse(text);
18704
+ } catch (e) {
18705
+ throw new Error(`${path6}: not valid JSON \u2014 ${e.message}`);
18706
+ }
18707
+ }
18708
+ function writeJson(path6, value) {
18709
+ mkdirSync3(dirname3(path6), { recursive: true });
18710
+ writeFileSync5(path6, JSON.stringify(value, null, 2) + "\n");
18711
+ }
18712
+ var profilePath = (root) => join3(root, QUALITY_DIR, PROFILE_FILE);
18713
+ var overlayPath = (root) => join3(root, QUALITY_DIR, OVERLAY_FILE);
18714
+ var auditsDir = (root) => join3(root, QUALITY_DIR, AUDITS_DIR);
18715
+ var auditDir = (root, auditId) => join3(auditsDir(root), auditId);
18716
+ function loadProfile(root) {
18717
+ const path6 = profilePath(root);
18718
+ return existsSync5(path6) ? readJson(path6) : null;
18719
+ }
18720
+ function loadOverlay(root) {
18721
+ const path6 = overlayPath(root);
18722
+ return existsSync5(path6) ? readJson(path6) : null;
18723
+ }
18724
+
18725
+ // ../schema/src/cli/kritik-audit.ts
18726
+ var SCORES_FILE = "scores.json";
18727
+ var FINDINGS_FILE = "findings.json";
18728
+ var MATRIX_FILE = "matrix.json";
18729
+ var scoresPath = (root, auditId) => join4(auditDir(root, auditId), SCORES_FILE);
18730
+ var findingsPath = (root, auditId) => join4(auditDir(root, auditId), FINDINGS_FILE);
18731
+ var matrixPath = (root, auditId) => join4(auditDir(root, auditId), MATRIX_FILE);
18732
+ function listAuditIds(root) {
18733
+ const dir = auditsDir(root);
18734
+ if (!existsSync6(dir)) return [];
18735
+ return readdirSync2(dir).filter((name) => statSync(join4(dir, name)).isDirectory()).sort();
18736
+ }
18737
+ function newestAuditId(root) {
18738
+ const dir = auditsDir(root);
18739
+ if (!existsSync6(dir)) throw new Error(`no audits directory at ${dir}`);
18740
+ const ids = listAuditIds(root);
18741
+ if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
18742
+ return ids[ids.length - 1];
18743
+ }
18744
+ function loadScores(root, auditId) {
18745
+ const path6 = scoresPath(root, auditId);
18746
+ if (!existsSync6(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
18747
+ const file2 = readJson(path6);
18748
+ return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
18749
+ }
18750
+ function loadScoresOrEmpty(root, auditId) {
18751
+ return existsSync6(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
18752
+ }
18753
+ function loadFindings(root, auditId) {
18754
+ const path6 = findingsPath(root, auditId);
18755
+ if (!existsSync6(path6)) return { audit_id: auditId, findings: [] };
18756
+ const file2 = readJson(path6);
18757
+ return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
18758
+ }
18759
+ function saveScores(root, auditId, file2) {
18760
+ writeJson(scoresPath(root, auditId), file2);
18761
+ }
18762
+ function saveFindings(root, auditId, file2) {
18763
+ writeJson(findingsPath(root, auditId), file2);
18764
+ }
18765
+ function requireProfile(root) {
18766
+ const profile = loadProfile(root);
18767
+ if (!profile) {
18768
+ throw new Error(
18769
+ `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).`
18770
+ );
18771
+ }
18772
+ return profile;
18773
+ }
18774
+ function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
18775
+ const findings = loadFindings(root, auditId);
18776
+ return {
18777
+ framework_version: scores.framework_version ?? library.version,
18778
+ profile: requireProfile(root),
18779
+ assessments: scores.assessments,
18780
+ findings: findings.findings
18781
+ };
18782
+ }
18783
+ function stripDerived(finding) {
18784
+ const { severity: _severity, priority: _priority, ...rest } = finding;
18785
+ void _severity;
18786
+ void _priority;
18787
+ return rest;
18788
+ }
18789
+ function requireAudit(root, auditId) {
18790
+ if (!listAuditIds(root).includes(auditId)) {
18791
+ throw new Error(`no audit "${auditId}" under ${auditsDir(root)}`);
18792
+ }
18793
+ }
18794
+ function loadCurrentQualitySection(root, library) {
18795
+ const auditIds = listAuditIds(root);
18796
+ if (auditIds.length === 0) return void 0;
18797
+ const profile = loadProfile(root);
18798
+ if (!profile) return void 0;
18799
+ const cells = /* @__PURE__ */ new Map();
18800
+ const findings = [];
18801
+ let frameworkVersion;
18802
+ for (const id of auditIds) {
18803
+ const scores = loadScoresOrEmpty(root, id);
18804
+ if (typeof scores.framework_version === "string") frameworkVersion = scores.framework_version;
18805
+ for (const assessment of scores.assessments) {
18806
+ cells.set(`${assessment.criterion_id}\0${assessment.surface}`, assessment);
18807
+ }
18808
+ for (const finding of loadFindings(root, id).findings) findings.push(stripDerived(finding));
18809
+ }
18810
+ return {
18811
+ framework_version: frameworkVersion ?? library.version,
18812
+ library,
18813
+ profile,
18814
+ assessments: [...cells.values()],
18815
+ findings
18816
+ };
18817
+ }
18818
+ function loadAuditQualitySection(root, auditId, library) {
18819
+ requireAudit(root, auditId);
18820
+ const section = loadQualitySection(root, auditId, library, loadScoresOrEmpty(root, auditId));
18821
+ return { ...section, library, findings: section.findings.map(stripDerived) };
18822
+ }
18823
+ function computeAuditMatrix(root, auditId, library) {
18824
+ const scores = loadScores(root, auditId);
18825
+ const section = loadQualitySection(root, auditId, library, scores);
18826
+ const matrix = deriveQualityMatrix({ quality: section }, library);
18827
+ const file2 = {
18828
+ audit_id: auditId,
18829
+ commit: scores.commit,
18830
+ framework_version: section.framework_version,
18831
+ matrix: matrix.matrix,
18832
+ overall: matrix.overall,
18833
+ finding_counts: matrix.finding_counts
18834
+ };
18835
+ writeJson(matrixPath(root, auditId), file2);
18836
+ return { section, matrix, file: file2 };
18837
+ }
18838
+ function renderMatrixMarkdown(matrix, domainNames) {
18839
+ const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
18840
+ const lines = [];
18841
+ lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
18842
+ lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
18843
+ for (const domain2 of matrix.domains) {
18844
+ const label = domainNames.get(domain2) ?? domain2;
18845
+ lines.push(
18846
+ `| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
18847
+ );
18848
+ }
18849
+ lines.push(
18850
+ `| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
18851
+ const score = matrix.overall[s];
18852
+ return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
18853
+ }).join(" | ")} |`
18854
+ );
18855
+ return lines.join("\n");
18856
+ }
18857
+ function locateFinding(root, id) {
18858
+ for (const auditId of [...listAuditIds(root)].reverse()) {
18859
+ const file2 = loadFindings(root, auditId);
18860
+ const finding = file2.findings.find((candidate) => candidate.id === id);
18861
+ if (finding) return { auditId, file: file2, finding };
18862
+ }
18863
+ return void 0;
18864
+ }
18865
+
18866
+ // src/lib/kritik-io.ts
18867
+ var KRITIK_ACTOR = "arkaik-cli";
18868
+ var DEFAULT_BUNDLE_PATH5 = join5("docs", "arkaik", "bundle.json");
18869
+ var VENDORED_PACK = join5(QUALITY_DIR, PACK_FILE);
18870
+ var BUNDLED_PACK = join5(dirname4(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
18871
+ function resolvePack(root) {
18872
+ const vendored = join5(root, VENDORED_PACK);
18873
+ if (existsSync7(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
18874
+ if (!existsSync7(BUNDLED_PACK)) {
18875
+ throw new Error(
18876
+ `no criteria pack found. Looked in:
18877
+ ${vendored}
18878
+ ${BUNDLED_PACK}
18879
+ The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
18880
+ );
18881
+ }
18882
+ return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
18883
+ }
18884
+ function loadKritikLibrary(root) {
18885
+ const pack = resolvePack(root);
18886
+ return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
18887
+ }
18888
+ function resolveJournal(root, bundlePath) {
18889
+ const resolved = bundlePath ?? join5(root, DEFAULT_BUNDLE_PATH5);
18890
+ return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync7(resolved) };
18891
+ }
18892
+ function appendQualityEvents(root, inputs, options = {}) {
18893
+ if (inputs.length === 0) return { events: [] };
18894
+ const actor = options.actor ?? KRITIK_ACTOR;
18895
+ const journal = resolveJournal(root, options.bundlePath);
18896
+ if (!journal.present) return { events: [] };
18897
+ const bundle = readBundle(journal.bundlePath);
18898
+ const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
18899
+ const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
18900
+ for (const event of events) appendJournalEvent(journal.journalPath, event);
18901
+ return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
18902
+ }
18903
+ function isQualitySection(value) {
18904
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18905
+ }
18906
+ function foldQualitySection(bundle, root, auditId) {
18907
+ const notFolded = (reason) => {
18908
+ const carriedSection = isQualitySection(bundle.quality);
18909
+ return {
18910
+ folded: false,
18911
+ carriedSection,
18912
+ notice: carriedSection ? `Quality: ${reason}
18913
+ Kept the quality section this bundle already carried \u2014 nothing replaced it.` : `Quality: ${reason}`
18914
+ };
18915
+ };
18916
+ if (auditId !== void 0) requireAudit(root, auditId);
18917
+ const auditIds = listAuditIds(root);
18918
+ if (auditIds.length === 0) {
18919
+ return notFolded(`none to fold \u2014 no audits under ${auditsDir(root)} (run \`arkaik kritik score\` to open one)`);
18920
+ }
18921
+ if (!loadProfile(root)) {
18922
+ return notFolded(`skipped, no profile \u2014 nothing at ${profilePath(root)} (run \`arkaik kritik profile\`)`);
18923
+ }
18924
+ let library;
18925
+ try {
18926
+ ({ library } = loadKritikLibrary(root));
18927
+ } catch (e) {
18928
+ return notFolded(`skipped, no pack \u2014 ${e.message}`);
18929
+ }
18930
+ const section = auditId === void 0 ? loadCurrentQualitySection(root, library) : loadAuditQualitySection(root, auditId, library);
18931
+ if (section === void 0) {
18932
+ return notFolded(`nothing to fold \u2014 no audits under ${auditsDir(root)}, or no profile at ${profilePath(root)}`);
18933
+ }
18934
+ bundle.quality = section;
18935
+ const count = auditId === void 0 ? auditIds.length : 1;
18936
+ return {
18937
+ folded: true,
18938
+ carriedSection: false,
18939
+ notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
18940
+ };
18941
+ }
18942
+ function resolveQualityRoot(options) {
18943
+ if (options.root !== void 0) return resolve4(options.fallback, options.root);
18944
+ const envRoot = (options.env ?? process.env).ARKAIK_QUALITY_ROOT;
18945
+ if (envRoot) return resolve4(envRoot);
18946
+ const dir = dirname4(options.bundlePath);
18947
+ if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
18948
+ return options.fallback;
18949
+ }
18950
+
18951
+ // src/commands/pack.ts
18952
+ var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
18953
+ var USAGE7 = `arkaik pack [--no-journal] [--no-quality] [--inline-assets] [--audit <id>]
18954
+ [--root <dir>] [--out <path>] [path]
18583
18955
 
18584
18956
  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.
18957
+ journal (or keep an existing embedded one), fold docs/quality/ into the
18958
+ quality section, and, with --inline-assets, inline local screenshot files as
18959
+ data: URIs. Written canonically via serializeBundle. Unknown top-level keys
18960
+ and unknown fields always round-trip.
18588
18961
 
18589
18962
  Arguments:
18590
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH5}).
18963
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
18591
18964
 
18592
18965
  Options:
18593
18966
  --no-journal Omit the embedded journal[] (Publik-safe posture \u2014 history
@@ -18596,12 +18969,33 @@ Options:
18596
18969
  interchange) \u2014 embedded wins over the sidecar when the
18597
18970
  bundle already carries one, otherwise the sidecar is used
18598
18971
  (same precedence "arkaik validate" folds by).
18972
+ --no-quality DELETE the quality section rather than folding one in \u2014
18973
+ not merely "skip the fold", because the source bundle may
18974
+ already carry a section of its own and that one goes too.
18975
+ For any bundle that must not travel with open findings:
18976
+ a finding names an unfixed vulnerability and the file to
18977
+ find it in. ("arkaik push" packs this way by default;
18978
+ its --include-quality opts back in.)
18979
+ Default: docs/quality/ IS folded in.
18599
18980
  --inline-assets Convert relative-path metadata.platformScreenshots values
18600
18981
  into data: URIs by reading the file from disk (resolved
18601
18982
  against the bundle's directory). Absolute https:// URLs
18602
18983
  and existing data: URIs are left as-is. v1 scope: local
18603
18984
  files only \u2014 uploading a remote/hosted copy is not
18604
18985
  implemented.
18986
+ --audit <id> Pin ONE audit's snapshot instead of the default merge.
18987
+ They answer different questions: the merge (every audit,
18988
+ latest score per criterion x surface) answers "where does
18989
+ the product stand", while a pinned audit answers "how did
18990
+ that audit go" \u2014 the question its own matrix.json
18991
+ answers. An id that is not on disk is an error, not an
18992
+ empty section.
18993
+ --root <dir> Where docs/quality/ lives. Default: NOT the current
18994
+ directory \u2014 it is derived from the bundle's own path, so
18995
+ packing <repo>/docs/arkaik/bundle.json folds <repo>'s
18996
+ audits whatever directory you run from. Only a bundle
18997
+ kept outside that conventional layout falls back to the
18998
+ cwd, and that is the case this flag is for.
18605
18999
  --out <path> Write the packed bundle here instead of stdout.
18606
19000
  -h, --help Show this help.`;
18607
19001
  function fail7(message) {
@@ -18639,7 +19033,7 @@ function fatalResult2(bundlePath, message) {
18639
19033
  }
18640
19034
  function runPack(options = {}) {
18641
19035
  const cwd = options.cwd ?? process.cwd();
18642
- const filePath = resolve3(cwd, options.path ?? DEFAULT_BUNDLE_PATH5);
19036
+ const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
18643
19037
  const noJournal = options.noJournal ?? false;
18644
19038
  const inlineAssets = options.inlineAssets ?? false;
18645
19039
  let bundle;
@@ -18660,10 +19054,24 @@ function runPack(options = {}) {
18660
19054
  journalEventCount = events.length;
18661
19055
  }
18662
19056
  }
19057
+ let qualityFolded;
19058
+ let qualityNotice;
19059
+ if (options.noQuality ?? false) {
19060
+ delete bundle.quality;
19061
+ } else {
19062
+ const root = resolveQualityRoot({ root: options.root, bundlePath: filePath, fallback: cwd });
19063
+ try {
19064
+ const fold = foldQualitySection(bundle, root, options.audit);
19065
+ qualityFolded = fold.folded;
19066
+ qualityNotice = fold.notice;
19067
+ } catch (e) {
19068
+ return fatalResult2(filePath, e.message);
19069
+ }
19070
+ }
18663
19071
  const inlinedAssets = [];
18664
19072
  const assetWarnings = [];
18665
19073
  if (inlineAssets) {
18666
- const bundleDir = dirname3(filePath);
19074
+ const bundleDir = dirname5(filePath);
18667
19075
  const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
18668
19076
  for (const node of nodes) {
18669
19077
  const nodeId = typeof node.id === "string" ? node.id : "?";
@@ -18674,12 +19082,12 @@ function runPack(options = {}) {
18674
19082
  const map2 = screenshots;
18675
19083
  for (const [platform, value] of Object.entries(map2)) {
18676
19084
  if (typeof value !== "string" || !isRelativeAssetPath(value)) continue;
18677
- const assetPath = resolve3(bundleDir, value);
18678
- if (!existsSync5(assetPath)) {
19085
+ const assetPath = resolve5(bundleDir, value);
19086
+ if (!existsSync8(assetPath)) {
18679
19087
  assetWarnings.push(`${nodeId}/${platform}: asset not found at ${assetPath} \u2014 left as-is`);
18680
19088
  continue;
18681
19089
  }
18682
- const bytes = readFileSync6(assetPath);
19090
+ const bytes = readFileSync7(assetPath);
18683
19091
  const mime = mimeForExtension(extname(assetPath));
18684
19092
  map2[platform] = `data:${mime};base64,${bytes.toString("base64")}`;
18685
19093
  inlinedAssets.push({ nodeId, platform, path: value });
@@ -18689,15 +19097,18 @@ function runPack(options = {}) {
18689
19097
  const output = serializeBundle(bundle);
18690
19098
  let outPath;
18691
19099
  if (options.out !== void 0) {
18692
- outPath = resolve3(cwd, options.out);
18693
- mkdirSync3(dirname3(outPath), { recursive: true });
18694
- writeFileSync5(outPath, output);
19100
+ outPath = resolve5(cwd, options.out);
19101
+ mkdirSync4(dirname5(outPath), { recursive: true });
19102
+ writeFileSync6(outPath, output);
18695
19103
  }
18696
- return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, output };
19104
+ return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, qualityFolded, qualityNotice, output };
18697
19105
  }
18698
19106
  function runPackCli(args) {
18699
19107
  let noJournal = false;
19108
+ let noQuality = false;
18700
19109
  let inlineAssets = false;
19110
+ let audit;
19111
+ let root;
18701
19112
  let out;
18702
19113
  const positionals = [];
18703
19114
  for (let i = 0; i < args.length; i++) {
@@ -18707,8 +19118,22 @@ function runPackCli(args) {
18707
19118
  process.exit(0);
18708
19119
  } else if (arg === "--no-journal") {
18709
19120
  noJournal = true;
19121
+ } else if (arg === "--no-quality") {
19122
+ noQuality = true;
18710
19123
  } else if (arg === "--inline-assets") {
18711
19124
  inlineAssets = true;
19125
+ } else if (arg === "--audit") {
19126
+ const value = args[++i];
19127
+ if (value === void 0) fail7(`Missing value for --audit
19128
+
19129
+ ${USAGE7}`);
19130
+ audit = value;
19131
+ } else if (arg === "--root") {
19132
+ const value = args[++i];
19133
+ if (value === void 0) fail7(`Missing value for --root
19134
+
19135
+ ${USAGE7}`);
19136
+ root = value;
18712
19137
  } else if (arg === "--out") {
18713
19138
  const value = args[++i];
18714
19139
  if (value === void 0) fail7(`Missing value for --out
@@ -18723,8 +19148,13 @@ ${USAGE7}`);
18723
19148
  positionals.push(arg);
18724
19149
  }
18725
19150
  }
18726
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH5;
18727
- const result = runPack({ path: filePath, out, noJournal, inlineAssets });
19151
+ if (noQuality && audit !== void 0) {
19152
+ fail7(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
19153
+
19154
+ ${USAGE7}`);
19155
+ }
19156
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
19157
+ const result = runPack({ path: filePath, out, noJournal, inlineAssets, noQuality, audit, root });
18728
19158
  if (!result.ok) fail7(`FATAL: ${result.fatal}`);
18729
19159
  if (result.journalIncluded) {
18730
19160
  console.error(`Journal: embedded ${result.journalEventCount} event(s)`);
@@ -18733,6 +19163,9 @@ ${USAGE7}`);
18733
19163
  } else {
18734
19164
  console.error("Journal: none to embed (no embedded journal, no sidecar)");
18735
19165
  }
19166
+ if (result.qualityNotice !== void 0) {
19167
+ console.error(result.qualityNotice);
19168
+ }
18736
19169
  for (const asset of result.inlinedAssets) {
18737
19170
  console.error(`Inlined asset: ${asset.nodeId}/${asset.platform} (${asset.path})`);
18738
19171
  }
@@ -18749,10 +19182,10 @@ ${USAGE7}`);
18749
19182
 
18750
19183
  // src/commands/open.ts
18751
19184
  import { spawn } from "node:child_process";
18752
- import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
19185
+ import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
18753
19186
  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";
19187
+ import { join as join6, resolve as resolve6 } from "node:path";
19188
+ var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
18756
19189
  var OPEN_URL = "https://arkaik.app/projects";
18757
19190
  var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
18758
19191
 
@@ -18763,7 +19196,7 @@ ${OPEN_URL} (the project list's "Import JSON" picker). On an invalid bundle,
18763
19196
  findings are printed and nothing is packed, written, or opened.
18764
19197
 
18765
19198
  Arguments:
18766
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
19199
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
18767
19200
 
18768
19201
  Options:
18769
19202
  --out <path> Write the packed bundle here instead of a temp file.
@@ -18785,7 +19218,7 @@ function fatalResult3(bundlePath, message) {
18785
19218
  }
18786
19219
  async function runOpen(options = {}) {
18787
19220
  const cwd = options.cwd ?? process.cwd();
18788
- const filePath = resolve4(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
19221
+ const filePath = resolve6(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
18789
19222
  const noOpen = options.noOpen ?? false;
18790
19223
  let v;
18791
19224
  try {
@@ -18804,9 +19237,9 @@ async function runOpen(options = {}) {
18804
19237
  }
18805
19238
  let outPath = packed.outPath;
18806
19239
  if (outPath === void 0) {
18807
- const dir = mkdtempSync(join3(tmpdir(), "arkaik-open-"));
18808
- outPath = join3(dir, "bundle.json");
18809
- writeFileSync6(outPath, packed.output);
19240
+ const dir = mkdtempSync(join6(tmpdir(), "arkaik-open-"));
19241
+ outPath = join6(dir, "bundle.json");
19242
+ writeFileSync7(outPath, packed.output);
18810
19243
  }
18811
19244
  let opened = false;
18812
19245
  if (!noOpen) {
@@ -18814,7 +19247,17 @@ async function runOpen(options = {}) {
18814
19247
  await opener(OPEN_URL);
18815
19248
  opened = true;
18816
19249
  }
18817
- return { ok: true, bundlePath: filePath, valid: true, errorLines, warningLines, outPath, url: OPEN_URL, opened };
19250
+ return {
19251
+ ok: true,
19252
+ bundlePath: filePath,
19253
+ valid: true,
19254
+ errorLines,
19255
+ warningLines,
19256
+ outPath,
19257
+ url: OPEN_URL,
19258
+ opened,
19259
+ qualityNotice: packed.qualityNotice
19260
+ };
18818
19261
  }
18819
19262
  function runOpenCli(args) {
18820
19263
  let out;
@@ -18841,7 +19284,7 @@ ${USAGE8}`);
18841
19284
  positionals.push(arg);
18842
19285
  }
18843
19286
  }
18844
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
19287
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
18845
19288
  runOpen({ path: filePath, out, noOpen }).then((result) => {
18846
19289
  if (!result.ok) fail8(`FATAL: ${result.fatal}`);
18847
19290
  if (result.warningLines.length > 0) {
@@ -18854,6 +19297,9 @@ ${USAGE8}`);
18854
19297
  console.error("\nInvalid bundle \u2014 not packed, not opened.");
18855
19298
  process.exit(1);
18856
19299
  }
19300
+ if (result.qualityNotice !== void 0) {
19301
+ console.error(result.qualityNotice);
19302
+ }
18857
19303
  console.log(`Packed -> ${result.outPath}`);
18858
19304
  if (result.opened) {
18859
19305
  console.log(`Opened ${result.url}`);
@@ -18865,10 +19311,10 @@ ${USAGE8}`);
18865
19311
  }
18866
19312
 
18867
19313
  // src/commands/push.ts
18868
- import { resolve as resolve5 } from "node:path";
18869
- var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
19314
+ import { resolve as resolve7 } from "node:path";
19315
+ var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
18870
19316
  var DEFAULT_API_BASE = "https://arkaik.app";
18871
- var USAGE9 = `arkaik push [--include-journal] [--api <base-url>] [path]
19317
+ var USAGE9 = `arkaik push [--include-journal] [--include-quality] [--api <base-url>] [path]
18872
19318
  arkaik push --delete <id> --key <owner_key> [--api <base-url>]
18873
19319
 
18874
19320
  Publish a project bundle to Publik (anonymous, account-less snapshot
@@ -18882,18 +19328,29 @@ Publik-safe posture (docs/spec/journal.md). --include-journal embeds it
18882
19328
  (like a bare "arkaik pack") and forwards ?include_journal=true so the server
18883
19329
  knows to keep it.
18884
19330
 
19331
+ A Kritik "quality" section is stripped by default too, and separately:
19332
+ publishing your history and publishing your open findings are two decisions,
19333
+ not one, and both default to no. --include-quality opts in.
19334
+
18885
19335
  Snapshots are immutable: there is no update verb. Pushing again always mints
18886
19336
  a new id. The owner key printed on success is shown exactly once and cannot
18887
19337
  be recovered \u2014 save it if you may need to delete the snapshot later.
18888
19338
 
18889
19339
  Arguments:
18890
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
19340
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH8}).
18891
19341
  Ignored with --delete.
18892
19342
 
18893
19343
  Options:
18894
19344
  --include-journal Embed the journal in the pushed bundle and forward
18895
19345
  ?include_journal=true. Default: stripped, omitted
18896
19346
  entirely from the request body.
19347
+ --include-quality Embed the Kritik quality section and forward
19348
+ ?include_quality=true. Opt-in rather than opt-out
19349
+ because an open finding is an unfixed vulnerability
19350
+ plus the path to find it in (docs/rfcs/kritik.md
19351
+ \xA7 8.3) \u2014 publishing that is a decision worth typing.
19352
+ Default: deleted before packing, so it is never in the
19353
+ request body at all.
18897
19354
  --api <base-url> Publik API base URL (default: ${DEFAULT_API_BASE}).
18898
19355
  Point at a self-hosted deployment.
18899
19356
  --delete <id> Delete a snapshot by id instead of pushing. Requires
@@ -18918,8 +19375,9 @@ function fatalResult4(bundlePath, message) {
18918
19375
  }
18919
19376
  async function runPush(options = {}) {
18920
19377
  const cwd = options.cwd ?? process.cwd();
18921
- const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
19378
+ const filePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
18922
19379
  const includeJournal = options.includeJournal ?? false;
19380
+ const includeQuality = options.includeQuality ?? false;
18923
19381
  const apiBase = options.apiBase ?? DEFAULT_API_BASE;
18924
19382
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
18925
19383
  let v;
@@ -18933,11 +19391,15 @@ async function runPush(options = {}) {
18933
19391
  if (!v.valid) {
18934
19392
  return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, requestSent: false };
18935
19393
  }
18936
- const packed = runPack({ path: filePath, noJournal: !includeJournal, cwd });
19394
+ const packed = runPack({ path: filePath, noJournal: !includeJournal, noQuality: !includeQuality, cwd });
18937
19395
  if (!packed.ok) {
18938
19396
  return fatalResult4(filePath, packed.fatal ?? "pack failed");
18939
19397
  }
18940
- const url2 = `${apiBase}/api/publik${includeJournal ? "?include_journal=true" : ""}`;
19398
+ const qualityNotice = packed.qualityNotice ?? "Quality: stripped, not sent (pass --include-quality to publish it)";
19399
+ const params = [];
19400
+ if (includeJournal) params.push("include_journal=true");
19401
+ if (includeQuality) params.push("include_quality=true");
19402
+ const url2 = `${apiBase}/api/publik${params.length > 0 ? `?${params.join("&")}` : ""}`;
18941
19403
  let res;
18942
19404
  try {
18943
19405
  res = await httpClient(url2, {
@@ -18967,6 +19429,7 @@ async function runPush(options = {}) {
18967
19429
  warningLines,
18968
19430
  requestSent: true,
18969
19431
  status,
19432
+ qualityNotice,
18970
19433
  id: body.id,
18971
19434
  url: body.url,
18972
19435
  ownerKey: body.owner_key
@@ -18985,6 +19448,7 @@ async function runPush(options = {}) {
18985
19448
  warningLines,
18986
19449
  requestSent: true,
18987
19450
  status,
19451
+ qualityNotice,
18988
19452
  serverFindings: errBody.findings,
18989
19453
  retryAfter: status === 429 ? res.headers.get("retry-after") : void 0,
18990
19454
  errorMessage: errBody.message ?? `Request failed with status ${status}`
@@ -18995,6 +19459,9 @@ function reportPush(result) {
18995
19459
  console.error(`Warnings: ${result.warningLines.length}`);
18996
19460
  result.warningLines.forEach((w) => console.error(` ${w}`));
18997
19461
  }
19462
+ if (result.qualityNotice !== void 0) {
19463
+ console.error(result.qualityNotice);
19464
+ }
18998
19465
  if (!result.valid) {
18999
19466
  console.error(`Errors: ${result.errorLines.length}`);
19000
19467
  result.errorLines.forEach((e) => console.error(` ${e}`));
@@ -19076,6 +19543,7 @@ function reportDelete(id, result) {
19076
19543
  }
19077
19544
  function runPushCli(args) {
19078
19545
  let includeJournal = false;
19546
+ let includeQuality = false;
19079
19547
  let apiBase;
19080
19548
  let deleteId;
19081
19549
  let key;
@@ -19087,6 +19555,8 @@ function runPushCli(args) {
19087
19555
  process.exit(0);
19088
19556
  } else if (arg === "--include-journal") {
19089
19557
  includeJournal = true;
19558
+ } else if (arg === "--include-quality") {
19559
+ includeQuality = true;
19090
19560
  } else if (arg === "--api") {
19091
19561
  const value = args[++i];
19092
19562
  if (value === void 0) fail9(`Missing value for --api
@@ -19128,16 +19598,16 @@ ${USAGE9}`);
19128
19598
  if (key !== void 0) fail9(`--key is only valid with --delete
19129
19599
 
19130
19600
  ${USAGE9}`);
19131
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
19132
- runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
19601
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH8;
19602
+ runPush({ path: filePath, includeJournal, includeQuality, apiBase }).then((result) => {
19133
19603
  if (!result.ok) fail9(`FATAL: ${result.fatal}`);
19134
19604
  reportPush(result);
19135
19605
  }).catch((e) => fail9(`FATAL: ${e.message}`));
19136
19606
  }
19137
19607
 
19138
19608
  // 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";
19609
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
19610
+ import { dirname as dirname6, join as join7, resolve as resolve8 } from "node:path";
19141
19611
  var LINK_FILE = "docs/arkaik/arkaik.json";
19142
19612
  var DEFAULT_BASE_URL = "https://arkaik.app";
19143
19613
  var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
@@ -19208,15 +19678,15 @@ async function runLink(argv, options = {}) {
19208
19678
  return { ok: false };
19209
19679
  }
19210
19680
  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 });
19681
+ const target = resolve8(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
19682
+ const linkPath = join7(target, LINK_FILE);
19683
+ mkdirSync5(dirname6(linkPath), { recursive: true });
19214
19684
  let existing = {};
19215
19685
  try {
19216
- existing = JSON.parse(readFileSync7(linkPath, "utf8"));
19686
+ existing = JSON.parse(readFileSync8(linkPath, "utf8"));
19217
19687
  } catch {
19218
19688
  }
19219
- writeFileSync7(
19689
+ writeFileSync8(
19220
19690
  linkPath,
19221
19691
  `${JSON.stringify({ ...existing, project_id: projectId, remote: baseUrl }, null, 2)}
19222
19692
  `
@@ -19243,14 +19713,15 @@ function runLinkCli(argv) {
19243
19713
  }
19244
19714
 
19245
19715
  // 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";
19716
+ import { existsSync as existsSync9, linkSync, mkdirSync as mkdirSync6, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
19717
+ import { join as join8, resolve as resolve9 } from "node:path";
19248
19718
  var LINK_FILE2 = "docs/arkaik/arkaik.json";
19249
- var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
19719
+ var DEFAULT_BUNDLE_PATH9 = "docs/arkaik/bundle.json";
19250
19720
  var DEFAULT_API_BASE2 = "https://arkaik.app";
19251
19721
  var USAGE11 = `arkaik restore [options] [path]
19252
19722
 
19253
- Replace the linked hosted project's bundle AND journal with a local bundle \u2014
19723
+ Replace the linked hosted project's bundle, journal AND quality section with
19724
+ a local bundle \u2014
19254
19725
  the landing step for a bootstrapped map. Before sending anything, this
19255
19726
  exports the CURRENT hosted state (snapshot + journal) to
19256
19727
  docs/arkaik/.backups/<timestamp>-bundle.json (next to the link file \u2014 not
@@ -19260,9 +19731,10 @@ only way back if the restore turns out to be wrong.
19260
19731
 
19261
19732
  Arguments:
19262
19733
  path Path to the local bundle JSON file
19263
- (default: ${DEFAULT_BUNDLE_PATH8}). Its journal.jsonl
19734
+ (default: ${DEFAULT_BUNDLE_PATH9}). Its journal.jsonl
19264
19735
  sidecar (or an embedded journal, which wins) is folded
19265
- in automatically.
19736
+ in automatically, as is the docs/quality/ tree of the
19737
+ repo that bundle belongs to.
19266
19738
 
19267
19739
  Options:
19268
19740
  --dry-run Ask the server what this restore WOULD do and print
@@ -19282,6 +19754,25 @@ Options:
19282
19754
  local copy (edited in the app), not an intended
19283
19755
  deletion. Undoing a restore from a backup is the
19284
19756
  common case where it IS intended.
19757
+ --no-quality Do not send a quality section: skip the docs/quality/
19758
+ fold AND drop any section the local bundle already
19759
+ carries. Does NOT by itself permit erasing the hosted
19760
+ one \u2014 that needs --allow-quality-loss too, because
19761
+ "don't send mine" and "destroy theirs" are different
19762
+ decisions and only one of them is irreversible.
19763
+ --allow-quality-loss Proceed even though the restore would erase a quality
19764
+ section the hosted project currently has. Without this
19765
+ flag, that refuses outright \u2014 it usually means
19766
+ docs/quality/ was looked for in the wrong place, which
19767
+ --root fixes, rather than an intended wipe.
19768
+ --audit <id> Fold ONE audit's snapshot rather than merging every
19769
+ audit into current state. An id that is not on disk is
19770
+ an error, not an empty section.
19771
+ --root <dir> Where docs/quality/ lives. Default: derived from the
19772
+ bundle's own path, so restoring <repo>/docs/arkaik/
19773
+ bundle.json folds <repo>'s audits whatever directory
19774
+ you run from; only a bundle kept outside that layout
19775
+ falls back to the cwd.
19285
19776
  --api <base-url> Override the remote from docs/arkaik/arkaik.json
19286
19777
  (also overridable with $ARKAIK_URL).
19287
19778
  -h, --help Show this help.
@@ -19294,8 +19785,8 @@ function fail10(message) {
19294
19785
  console.error(message);
19295
19786
  process.exit(1);
19296
19787
  }
19297
- function fatalResult5(dryRun, message) {
19298
- return { ok: false, fatal: message, dryRun, requestSent: false };
19788
+ function fatalResult5(dryRun, message, quality = {}) {
19789
+ return { ok: false, fatal: message, dryRun, requestSent: false, ...quality };
19299
19790
  }
19300
19791
  async function safeJson(res) {
19301
19792
  try {
@@ -19321,7 +19812,9 @@ async function interpretPutResponse(res, ctx) {
19321
19812
  bundlePath: ctx.bundlePath,
19322
19813
  backupPath: ctx.backupPath,
19323
19814
  requestSent: true,
19324
- status: res.status
19815
+ status: res.status,
19816
+ qualityFolded: ctx.qualityFolded,
19817
+ qualityNotice: ctx.qualityNotice
19325
19818
  };
19326
19819
  const backupNote = backupNoteFor(ctx.backupPath);
19327
19820
  if (res.status === 200) {
@@ -19385,7 +19878,7 @@ async function interpretPutResponse(res, ctx) {
19385
19878
  }
19386
19879
  function writeBackupFile(filePath, content) {
19387
19880
  const tmpPath = `${filePath}.tmp-${process.pid}`;
19388
- writeFileSync8(tmpPath, content);
19881
+ writeFileSync9(tmpPath, content);
19389
19882
  try {
19390
19883
  linkSync(tmpPath, filePath);
19391
19884
  } finally {
@@ -19426,20 +19919,50 @@ function describeDeletions(removedNodes, removedEdges, bundlePath) {
19426
19919
  );
19427
19920
  return lines.join("\n");
19428
19921
  }
19922
+ function describeQualityLoss(hostedQuality, qualityRoot, noQuality, foldNotice) {
19923
+ const findings = Array.isArray(hostedQuality.findings) ? hostedQuality.findings.length : 0;
19924
+ const assessments = Array.isArray(hostedQuality.assessments) ? hostedQuality.assessments.length : 0;
19925
+ const parts = [];
19926
+ if (findings > 0) parts.push(`${findings} open finding${findings === 1 ? "" : "s"}`);
19927
+ if (assessments > 0) parts.push(`${assessments} assessment${assessments === 1 ? "" : "s"}`);
19928
+ const held = parts.length > 0 ? parts.join(" and ") : "no findings or assessments";
19929
+ const lines = [
19930
+ `This restore would ERASE the hosted project's quality section (${held}). Nothing was sent.`
19931
+ ];
19932
+ if (noQuality) {
19933
+ lines.push(
19934
+ `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.`
19935
+ );
19936
+ } else {
19937
+ if (foldNotice !== void 0) {
19938
+ lines.push("The outbound bundle has no section because the fold found nothing to build one from:");
19939
+ lines.push(` ${foldNotice}`);
19940
+ }
19941
+ if (!existsSync9(join8(qualityRoot, QUALITY_DIR))) {
19942
+ lines.push(
19943
+ `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.`
19944
+ );
19945
+ }
19946
+ lines.push(`If the hosted section really is meant to go, re-run with --allow-quality-loss.`);
19947
+ }
19948
+ return lines.join("\n");
19949
+ }
19429
19950
  async function runRestore(options = {}) {
19430
19951
  const cwd = options.cwd ?? process.cwd();
19431
19952
  const env = options.env ?? process.env;
19432
19953
  const dryRun = options.dryRun ?? false;
19433
19954
  const allowHistoryLoss = options.allowHistoryLoss ?? false;
19434
19955
  const allowDeletions = options.allowDeletions ?? false;
19956
+ const noQuality = options.noQuality ?? false;
19957
+ const allowQualityLoss = options.allowQualityLoss ?? false;
19435
19958
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
19436
- const linkPath = join5(cwd, LINK_FILE2);
19437
- if (!existsSync6(linkPath)) {
19959
+ const linkPath = join8(cwd, LINK_FILE2);
19960
+ if (!existsSync9(linkPath)) {
19438
19961
  return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
19439
19962
  }
19440
19963
  let link;
19441
19964
  try {
19442
- link = JSON.parse(readFileSync8(linkPath, "utf8"));
19965
+ link = JSON.parse(readFileSync9(linkPath, "utf8"));
19443
19966
  } catch (e) {
19444
19967
  return fatalResult5(dryRun, `Could not parse ${LINK_FILE2}: ${e.message}`);
19445
19968
  }
@@ -19449,11 +19972,11 @@ async function runRestore(options = {}) {
19449
19972
  const encodedProjectId = encodeURIComponent(projectId);
19450
19973
  const token = env.ARKAIK_TOKEN;
19451
19974
  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.`);
19975
+ const bundlePath = resolve9(cwd, options.path ?? DEFAULT_BUNDLE_PATH9);
19976
+ if (!existsSync9(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
19454
19977
  let localRaw;
19455
19978
  try {
19456
- localRaw = JSON.parse(readFileSync8(bundlePath, "utf8"));
19979
+ localRaw = JSON.parse(readFileSync9(bundlePath, "utf8"));
19457
19980
  } catch (e) {
19458
19981
  return fatalResult5(dryRun, `Could not parse ${bundlePath}: ${e.message}`);
19459
19982
  }
@@ -19463,6 +19986,22 @@ async function runRestore(options = {}) {
19463
19986
  const local = localRaw;
19464
19987
  const journalEvents = loadJournalEvents(local, bundlePath);
19465
19988
  const outboundBundle = { ...local, journal: journalEvents };
19989
+ const qualityRoot = resolveQualityRoot({ root: options.root, bundlePath, fallback: cwd });
19990
+ let qualityFolded = false;
19991
+ let qualityNotice;
19992
+ if (noQuality) {
19993
+ delete outboundBundle.quality;
19994
+ qualityNotice = "Quality: deleted before sending (--no-quality)";
19995
+ } else {
19996
+ try {
19997
+ const fold = foldQualitySection(outboundBundle, qualityRoot, options.audit);
19998
+ qualityFolded = fold.folded;
19999
+ qualityNotice = fold.notice;
20000
+ } catch (e) {
20001
+ return fatalResult5(dryRun, e.message);
20002
+ }
20003
+ }
20004
+ const qualityFields = { qualityFolded, qualityNotice };
19466
20005
  const headers = { Authorization: `Bearer ${token}` };
19467
20006
  let version2;
19468
20007
  try {
@@ -19491,7 +20030,7 @@ async function runRestore(options = {}) {
19491
20030
  } catch (e) {
19492
20031
  return { ok: true, dryRun, bundlePath, requestSent: false, errorMessage: `Network error: ${e.message}` };
19493
20032
  }
19494
- return interpretPutResponse(res2, { dryRun, bundlePath, version: version2 });
20033
+ return interpretPutResponse(res2, { dryRun, bundlePath, version: version2, ...qualityFields });
19495
20034
  }
19496
20035
  let exported;
19497
20036
  try {
@@ -19518,28 +20057,34 @@ async function runRestore(options = {}) {
19518
20057
  if (journalEvents.length < hostedEventCount && !allowHistoryLoss) {
19519
20058
  return fatalResult5(
19520
20059
  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.`
20060
+ `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.`,
20061
+ qualityFields
19522
20062
  );
19523
20063
  }
20064
+ const hostedQuality = exportedBundle.quality;
20065
+ if (isQualitySection(hostedQuality) && !isQualitySection(outboundBundle.quality) && !allowQualityLoss) {
20066
+ return fatalResult5(dryRun, describeQualityLoss(hostedQuality, qualityRoot, noQuality, qualityNotice), qualityFields);
20067
+ }
19524
20068
  const removedNodes = removedIds(exportedBundle.nodes, Array.isArray(local.nodes) ? local.nodes : []);
19525
20069
  const removedEdges = removedIds(exportedBundle.edges, Array.isArray(local.edges) ? local.edges : []);
19526
20070
  if ((removedNodes.length > 0 || removedEdges.length > 0) && !allowDeletions) {
19527
- return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath));
20071
+ return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath), qualityFields);
19528
20072
  }
19529
- const backupDir = join5(cwd, "docs", "arkaik", ".backups");
20073
+ const backupDir = join8(cwd, "docs", "arkaik", ".backups");
19530
20074
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19531
- const backupPath = join5(backupDir, `${stamp}-bundle.json`);
20075
+ const backupPath = join8(backupDir, `${stamp}-bundle.json`);
19532
20076
  const backupContent = `${JSON.stringify(exported, null, 2)}
19533
20077
  `;
19534
20078
  try {
19535
- mkdirSync5(backupDir, { recursive: true });
20079
+ mkdirSync6(backupDir, { recursive: true });
19536
20080
  writeBackupFile(backupPath, backupContent);
19537
- JSON.parse(readFileSync8(backupPath, "utf8"));
20081
+ JSON.parse(readFileSync9(backupPath, "utf8"));
19538
20082
  } catch (e) {
19539
20083
  return fatalResult5(
19540
20084
  dryRun,
19541
20085
  `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.`
20086
+ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`,
20087
+ qualityFields
19543
20088
  );
19544
20089
  }
19545
20090
  let res;
@@ -19559,7 +20104,7 @@ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND
19559
20104
  errorMessage: `Network error: ${e.message}. Nothing was sent.${backupNoteFor(backupPath)}`
19560
20105
  };
19561
20106
  }
19562
- return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2 });
20107
+ return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2, ...qualityFields });
19563
20108
  }
19564
20109
  function printDelta(delta) {
19565
20110
  if (!delta) return;
@@ -19590,6 +20135,9 @@ function reportRestore(result) {
19590
20135
  if (result.backupPath) {
19591
20136
  console.log(`Backed up the current hosted project (snapshot + journal) to ${result.backupPath}`);
19592
20137
  }
20138
+ if (result.qualityNotice !== void 0) {
20139
+ console.log(result.qualityNotice);
20140
+ }
19593
20141
  if (!result.requestSent) {
19594
20142
  console.error(result.errorMessage ?? "Restore failed before a request could be sent.");
19595
20143
  process.exit(1);
@@ -19598,7 +20146,9 @@ function reportRestore(result) {
19598
20146
  if (result.dryRun) {
19599
20147
  console.log("[dry-run] server preview \u2014 nothing was written:");
19600
20148
  printDelta(result.delta);
19601
- console.log("Re-run without --dry-run to apply \u2014 that run takes the backup.");
20149
+ console.log(
20150
+ "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)."
20151
+ );
19602
20152
  } else {
19603
20153
  console.log(`Restored. New version ${result.version}.`);
19604
20154
  printDelta(result.delta);
@@ -19619,6 +20169,10 @@ function runRestoreCli(argv) {
19619
20169
  let dryRun = false;
19620
20170
  let allowHistoryLoss = false;
19621
20171
  let allowDeletions = false;
20172
+ let noQuality = false;
20173
+ let allowQualityLoss = false;
20174
+ let audit;
20175
+ let root;
19622
20176
  let apiBase;
19623
20177
  const positionals = [];
19624
20178
  for (let i = 0; i < argv.length; i++) {
@@ -19633,6 +20187,22 @@ function runRestoreCli(argv) {
19633
20187
  allowHistoryLoss = true;
19634
20188
  } else if (arg === "--allow-deletions") {
19635
20189
  allowDeletions = true;
20190
+ } else if (arg === "--no-quality") {
20191
+ noQuality = true;
20192
+ } else if (arg === "--allow-quality-loss") {
20193
+ allowQualityLoss = true;
20194
+ } else if (arg === "--audit") {
20195
+ const value = argv[++i];
20196
+ if (value === void 0) fail10(`Missing value for --audit
20197
+
20198
+ ${USAGE11}`);
20199
+ audit = value;
20200
+ } else if (arg === "--root") {
20201
+ const value = argv[++i];
20202
+ if (value === void 0) fail10(`Missing value for --root
20203
+
20204
+ ${USAGE11}`);
20205
+ root = value;
19636
20206
  } else if (arg === "--api") {
19637
20207
  const value = argv[++i];
19638
20208
  if (value === void 0) fail10(`Missing value for --api
@@ -19650,28 +20220,33 @@ ${USAGE11}`);
19650
20220
  if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
19651
20221
 
19652
20222
  ${USAGE11}`);
19653
- runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
20223
+ if (noQuality && audit !== void 0) {
20224
+ fail10(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
20225
+
20226
+ ${USAGE11}`);
20227
+ }
20228
+ runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, noQuality, allowQualityLoss, audit, root, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
19654
20229
  }
19655
20230
 
19656
20231
  // src/commands/bootstrap.ts
19657
20232
  import { spawnSync as spawnSync2 } from "node:child_process";
19658
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, renameSync, writeFileSync as writeFileSync12 } from "node:fs";
20233
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync, writeFileSync as writeFileSync13 } from "node:fs";
19659
20234
  import path5 from "node:path";
19660
20235
 
19661
20236
  // src/lib/bootstrap/corpus.ts
19662
20237
  import { spawnSync } from "node:child_process";
19663
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
20238
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
19664
20239
  import path2 from "node:path";
19665
20240
 
19666
20241
  // src/lib/bootstrap/paths.ts
19667
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
20242
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
19668
20243
  import path from "node:path";
19669
20244
  var BOOTSTRAP_ROOT = ".arkaik";
19670
20245
  var CORPUS_DIR = ".arkaik/corpus";
19671
20246
  var PLAN_DIR = ".arkaik/bootstrap";
19672
20247
  var FRAGMENTS_DIR = ".arkaik/bootstrap/fragments";
19673
20248
  var MANIFEST_FILE = ".arkaik/bootstrap/manifest.json";
19674
- var PROFILE_FILE = ".arkaik/bootstrap/profile.json";
20249
+ var PROFILE_FILE2 = ".arkaik/bootstrap/profile.json";
19675
20250
  var PRS_FILE = ".arkaik/corpus/prs.jsonl";
19676
20251
  var DOCS_FILE = ".arkaik/corpus/docs.json";
19677
20252
  var SURFACES_FILE = ".arkaik/corpus/surfaces.json";
@@ -19679,16 +20254,16 @@ function at(cwd, relative) {
19679
20254
  return path.join(cwd, relative);
19680
20255
  }
19681
20256
  function ensureDir(dirPath) {
19682
- mkdirSync6(dirPath, { recursive: true });
20257
+ mkdirSync7(dirPath, { recursive: true });
19683
20258
  }
19684
20259
  function ensureGitignored(cwd) {
19685
20260
  const file2 = path.join(cwd, ".gitignore");
19686
20261
  const line2 = `${BOOTSTRAP_ROOT}/`;
19687
- const current = existsSync7(file2) ? readFileSync9(file2, "utf8") : "";
20262
+ const current = existsSync10(file2) ? readFileSync10(file2, "utf8") : "";
19688
20263
  const ignored = current.split("\n").map((l) => l.trim()).some((l) => l === line2 || l === BOOTSTRAP_ROOT);
19689
20264
  if (ignored) return false;
19690
20265
  const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
19691
- writeFileSync9(file2, `${current}${prefix}${line2}
20266
+ writeFileSync10(file2, `${current}${prefix}${line2}
19692
20267
  `);
19693
20268
  return true;
19694
20269
  }
@@ -19785,7 +20360,7 @@ function fetchPrsViaGit(cwd) {
19785
20360
  function walk(root, cwd, out) {
19786
20361
  let entries;
19787
20362
  try {
19788
- entries = readdirSync2(root, { withFileTypes: true });
20363
+ entries = readdirSync3(root, { withFileTypes: true });
19789
20364
  } catch {
19790
20365
  return;
19791
20366
  }
@@ -19803,7 +20378,7 @@ function listFiles(cwd) {
19803
20378
  }
19804
20379
  function buildDocsManifest(cwd, files) {
19805
20380
  return files.filter((f) => f.startsWith("docs/") && f.endsWith(".md")).map((f) => {
19806
- const text = readFileSync10(path2.join(cwd, f), "utf8");
20381
+ const text = readFileSync11(path2.join(cwd, f), "utf8");
19807
20382
  const heading = /^#\s+(.+)$/m.exec(text);
19808
20383
  return { path: f, title: heading ? heading[1].trim() : path2.basename(f, ".md") };
19809
20384
  });
@@ -19818,7 +20393,7 @@ function buildSurfaceInventory(files) {
19818
20393
  }
19819
20394
  function buildCorpus(options) {
19820
20395
  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);
20396
+ const raw = options.fromJson ? JSON.parse(readFileSync11(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
19822
20397
  let prs = normalizePrs(raw);
19823
20398
  let sinceDroppedUndated = 0;
19824
20399
  if (options.since) {
@@ -19839,21 +20414,21 @@ function buildCorpus(options) {
19839
20414
  const docs = buildDocsManifest(cwd, files);
19840
20415
  const surfaces = buildSurfaceInventory(files);
19841
20416
  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)}
20417
+ writeFileSync11(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
20418
+ writeFileSync11(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
19844
20419
  `);
19845
- writeFileSync10(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
20420
+ writeFileSync11(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
19846
20421
  `);
19847
20422
  return { prs: prs.length, docs: docs.length, surfaces: surfaces.length, sinceDroppedUndated };
19848
20423
  }
19849
20424
  function readCorpusPrs(cwd) {
19850
20425
  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));
20426
+ if (!existsSync11(file2)) return [];
20427
+ return readFileSync11(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
19853
20428
  }
19854
20429
 
19855
20430
  // src/lib/bootstrap/fragments.ts
19856
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
20431
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
19857
20432
  import path3 from "node:path";
19858
20433
  function isArrayOfObjects(value) {
19859
20434
  return value === void 0 || Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null && !Array.isArray(v));
@@ -19879,13 +20454,13 @@ function loadFragments(cwd, manifest) {
19879
20454
  problems.push({ unit: String(unit2.id), message: err instanceof Error ? err.message : String(err) });
19880
20455
  continue;
19881
20456
  }
19882
- if (!existsSync9(file2)) {
20457
+ if (!existsSync12(file2)) {
19883
20458
  missing.push(unit2.id);
19884
20459
  continue;
19885
20460
  }
19886
20461
  let parsed;
19887
20462
  try {
19888
- parsed = JSON.parse(readFileSync11(file2, "utf8"));
20463
+ parsed = JSON.parse(readFileSync12(file2, "utf8"));
19889
20464
  } catch (err) {
19890
20465
  problems.push({ unit: unit2.id, message: `not valid JSON: ${err instanceof Error ? err.message : "parse error"}` });
19891
20466
  continue;
@@ -19922,7 +20497,7 @@ function renderIndex(bundle) {
19922
20497
  }
19923
20498
 
19924
20499
  // src/lib/bootstrap/manifest.ts
19925
- import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "node:fs";
20500
+ import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "node:fs";
19926
20501
  import path4 from "node:path";
19927
20502
 
19928
20503
  // src/lib/bootstrap/era-window.ts
@@ -20030,19 +20605,19 @@ function assertValidProfile(profile) {
20030
20605
 
20031
20606
  // src/lib/bootstrap/manifest.ts
20032
20607
  function readProfile(cwd) {
20033
- const file2 = at(cwd, PROFILE_FILE);
20034
- if (!existsSync10(file2)) return null;
20608
+ const file2 = at(cwd, PROFILE_FILE2);
20609
+ if (!existsSync13(file2)) return null;
20035
20610
  try {
20036
- return JSON.parse(readFileSync12(file2, "utf8"));
20611
+ return JSON.parse(readFileSync13(file2, "utf8"));
20037
20612
  } catch (err) {
20038
- throw new Error(`cannot read ${PROFILE_FILE}: ${err instanceof Error ? err.message : String(err)}`);
20613
+ throw new Error(`cannot read ${PROFILE_FILE2}: ${err instanceof Error ? err.message : String(err)}`);
20039
20614
  }
20040
20615
  }
20041
20616
  function readManifest(cwd) {
20042
20617
  const file2 = at(cwd, MANIFEST_FILE);
20043
- if (!existsSync10(file2)) return null;
20618
+ if (!existsSync13(file2)) return null;
20044
20619
  try {
20045
- return JSON.parse(readFileSync12(file2, "utf8"));
20620
+ return JSON.parse(readFileSync13(file2, "utf8"));
20046
20621
  } catch (err) {
20047
20622
  throw new Error(`cannot read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
20048
20623
  }
@@ -20050,15 +20625,15 @@ function readManifest(cwd) {
20050
20625
  function writeManifest(cwd, manifest) {
20051
20626
  ensureDir(at(cwd, PLAN_DIR));
20052
20627
  ensureDir(at(cwd, FRAGMENTS_DIR));
20053
- writeFileSync11(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
20628
+ writeFileSync12(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
20054
20629
  `);
20055
20630
  }
20056
20631
  function detectMode(cwd, bundlePath) {
20057
20632
  const file2 = path4.resolve(cwd, bundlePath);
20058
- if (!existsSync10(file2)) return "greenfield";
20633
+ if (!existsSync13(file2)) return "greenfield";
20059
20634
  let parsed;
20060
20635
  try {
20061
- parsed = JSON.parse(readFileSync12(file2, "utf8"));
20636
+ parsed = JSON.parse(readFileSync13(file2, "utf8"));
20062
20637
  } catch (err) {
20063
20638
  throw new Error(`cannot read bundle at ${bundlePath}: ${err instanceof Error ? err.message : String(err)}`);
20064
20639
  }
@@ -20511,7 +21086,7 @@ function mergeFragments(input) {
20511
21086
  }
20512
21087
 
20513
21088
  // src/lib/bootstrap/slice.ts
20514
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
21089
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
20515
21090
 
20516
21091
  // src/lib/bootstrap/body-budget.ts
20517
21092
  var LAB_NOTE_HEADING_RE = /^##\s+Lab Note.*$/m;
@@ -20553,8 +21128,8 @@ function boundBody(body) {
20553
21128
 
20554
21129
  // src/lib/bootstrap/slice.ts
20555
21130
  function readJsonArray(file2) {
20556
- if (!existsSync11(file2)) return [];
20557
- const parsed = JSON.parse(readFileSync13(file2, "utf8"));
21131
+ if (!existsSync14(file2)) return [];
21132
+ const parsed = JSON.parse(readFileSync14(file2, "utf8"));
20558
21133
  return Array.isArray(parsed) ? parsed : [];
20559
21134
  }
20560
21135
  function toPosix(value) {
@@ -20575,7 +21150,7 @@ function eraWindows(cwd, slugs) {
20575
21150
  const era = bySlug.get(slug);
20576
21151
  if (!era) {
20577
21152
  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.`
21153
+ `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
21154
  );
20580
21155
  }
20581
21156
  assertEraWindow(era);
@@ -20664,7 +21239,7 @@ ${usage}`);
20664
21239
  }
20665
21240
  function writeFileAtomic(filePath, content) {
20666
21241
  const tmpPath = `${filePath}.tmp-${process.pid}`;
20667
- writeFileSync12(tmpPath, content);
21242
+ writeFileSync13(tmpPath, content);
20668
21243
  renameSync(tmpPath, filePath);
20669
21244
  }
20670
21245
  function runCorpus(argv) {
@@ -20699,7 +21274,7 @@ ${CORPUS_USAGE}`);
20699
21274
  ${CORPUS_USAGE}`);
20700
21275
  }
20701
21276
  }
20702
- if (!existsSync12(path5.join(cwd, ".git"))) {
21277
+ if (!existsSync15(path5.join(cwd, ".git"))) {
20703
21278
  fail11("`arkaik bootstrap corpus` must run from the repository root (no .git here).");
20704
21279
  }
20705
21280
  try {
@@ -20761,7 +21336,7 @@ ${PLAN_USAGE}`);
20761
21336
 
20762
21337
  ${PLAN_USAGE}`);
20763
21338
  }
20764
- if (!existsSync12(path5.join(cwd, ".git"))) {
21339
+ if (!existsSync15(path5.join(cwd, ".git"))) {
20765
21340
  fail11("`arkaik bootstrap plan` must run from the repository root (no .git here).");
20766
21341
  }
20767
21342
  try {
@@ -20900,7 +21475,7 @@ ${MERGE_USAGE}`);
20900
21475
  }
20901
21476
  try {
20902
21477
  const bundlePath = path5.resolve(cwd, manifest.bundle);
20903
- const base = existsSync12(bundlePath) ? readBundle(bundlePath) : {
21478
+ const base = existsSync15(bundlePath) ? readBundle(bundlePath) : {
20904
21479
  schema_version: 3,
20905
21480
  project: {
20906
21481
  id: path5.basename(cwd),
@@ -20936,7 +21511,7 @@ ${MERGE_USAGE}`);
20936
21511
  const journalPath = journalPathFor(bundlePath);
20937
21512
  const journalText = result.journal.map((e) => JSON.stringify(e)).join("\n") + (result.journal.length ? "\n" : "");
20938
21513
  if (!dryRun) {
20939
- mkdirSync7(path5.dirname(bundlePath), { recursive: true });
21514
+ mkdirSync8(path5.dirname(bundlePath), { recursive: true });
20940
21515
  writeFileAtomic(bundlePath, serialized);
20941
21516
  writeFileAtomic(journalPath, journalText);
20942
21517
  }
@@ -20985,138 +21560,6 @@ ${USAGE12}`);
20985
21560
  }
20986
21561
  }
20987
21562
 
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
21563
  // ../schema/src/cli/kritik-overlay.ts
21121
21564
  var ANCHOR_KEYS = ["l0", "l1", "l2", "l3", "l4"];
21122
21565
  var CRITERION_TEMPLATE = {
@@ -21247,49 +21690,6 @@ Pass --domain-name "<display name>" to define it, or use an existing domain code
21247
21690
 
21248
21691
  // src/commands/kritik.ts
21249
21692
  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
21693
  var USAGE13 = `arkaik kritik <subcommand> [options]
21294
21694
 
21295
21695
  Audit this product's quality with the Kritik framework: a maturity level per
@@ -21304,6 +21704,7 @@ Subcommands:
21304
21704
  finding accept <id> Accept it as a known, owned risk.
21305
21705
  matrix [audit-id] Roll an audit up (writes matrix.json).
21306
21706
  signals The signal pack, and what has tripped since the last audit.
21707
+ regressions What got worse between two audits.
21307
21708
  issue <criterion> Print the prefilled GitHub issue skeleton.
21308
21709
  criterion add ... Add a project-specific criterion to the overlay.
21309
21710
 
@@ -21377,6 +21778,20 @@ makes it usable as a CI step.
21377
21778
  --trip Record one as tripped: appends quality.signal.tripped.
21378
21779
  --signal takes the row's index from the run sheet, or the text.
21379
21780
  --json The full run sheet as JSON (what an agent should read).`;
21781
+ var REGRESSIONS_USAGE = `arkaik kritik regressions [--from <audit>] [--to <audit>] [--record] [--json]
21782
+
21783
+ What got worse between two audits: a cell whose maturity dropped, a cell that
21784
+ gained an open Critical or High finding, a finding that was resolved and is open
21785
+ again. Cells scored in only one of the two audits are not compared \u2014 a
21786
+ half-finished audit is not a regression.
21787
+
21788
+ Exits 1 when anything regressed, which is what makes it usable as a CI step or a
21789
+ scheduled routine.
21790
+
21791
+ --from <audit> The older reading (default: the audit before --to).
21792
+ --to <audit> The newer reading (default: the newest on disk).
21793
+ --record Append one quality.signal.tripped per regression.
21794
+ --json The full list as JSON.`;
21380
21795
  var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
21381
21796
 
21382
21797
  Print the criterion's GitHub issue skeleton, filled as far as what we know
@@ -21930,7 +22345,8 @@ function runSignals(args, common) {
21930
22345
  console.log(
21931
22346
  `
21932
22347
  ${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.`
22348
+ Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.` + (listAuditIds(common.root).length > 1 ? `
22349
+ Comparing two audits is \`arkaik kritik regressions\`.` : "")
21934
22350
  );
21935
22351
  }
21936
22352
  if (trips.length > 0) {
@@ -21946,6 +22362,83 @@ function runSignals(args, common) {
21946
22362
  console.log("");
21947
22363
  process.exit(0);
21948
22364
  }
22365
+ function auditPair(root, from, to) {
22366
+ const audits = listAuditIds(root);
22367
+ if (audits.length < 2) {
22368
+ fail12(
22369
+ `kritik: regressions needs two audits to compare \u2014 ${audits.length === 0 ? "docs/quality/audits/ holds none" : `only "${audits[0]}" exists`}.
22370
+ A regression is the difference between two readings; one reading is a baseline.`
22371
+ );
22372
+ }
22373
+ const known = (id) => {
22374
+ if (!audits.includes(id)) fail12(`kritik: no audit "${id}" under docs/quality/audits/ (have: ${audits.join(", ")})`);
22375
+ return id;
22376
+ };
22377
+ const newer = to === void 0 ? audits[audits.length - 1] : known(to);
22378
+ const older = from === void 0 ? audits[audits.indexOf(newer) - 1] : known(from);
22379
+ if (older === void 0) {
22380
+ fail12(`kritik: "${newer}" is the oldest audit \u2014 there is nothing before it to compare against.`);
22381
+ }
22382
+ if (older === newer) {
22383
+ fail12(`kritik: --from and --to name the same audit ("${newer}") \u2014 a regression needs two readings.`);
22384
+ }
22385
+ if (audits.indexOf(older) > audits.indexOf(newer)) {
22386
+ fail12(`kritik: --from "${older}" is newer than --to "${newer}" \u2014 swap them, or the comparison inverts.`);
22387
+ }
22388
+ return { from: older, to: newer };
22389
+ }
22390
+ function runRegressions(args, common) {
22391
+ const { single, flags } = collect(args, [], ["json", "record"]);
22392
+ if (flags.has("help")) {
22393
+ console.log(REGRESSIONS_USAGE);
22394
+ process.exit(0);
22395
+ }
22396
+ const library = loadLibraryOrFail(common.root);
22397
+ profileOrFail(common.root);
22398
+ const { from, to } = auditPair(common.root, single.from, single.to);
22399
+ let regressions;
22400
+ try {
22401
+ regressions = detectRegressions(
22402
+ loadQualitySection(common.root, from, library),
22403
+ loadQualitySection(common.root, to, library),
22404
+ library
22405
+ );
22406
+ } catch (error51) {
22407
+ return fail12(`kritik: ${error51.message}`);
22408
+ }
22409
+ if (flags.has("json")) {
22410
+ console.log(JSON.stringify({ from, to, total: regressions.length, regressions }, null, 2));
22411
+ } else if (regressions.length === 0) {
22412
+ console.log(`
22413
+ nothing regressed between ${from} and ${to}.
22414
+ `);
22415
+ } else {
22416
+ console.log("");
22417
+ for (const regression of regressions) {
22418
+ console.log(` [${regression.kind}] ${regression.criterion_id} x ${regression.surface}`);
22419
+ console.log(` ${regression.detail}`);
22420
+ }
22421
+ console.log(`
22422
+ ${regressions.length} regression${regressions.length === 1 ? "" : "s"} between ${from} and ${to}.`);
22423
+ }
22424
+ if (flags.has("record") && regressions.length > 0) {
22425
+ reportJournal(
22426
+ common.root,
22427
+ regressions.map(
22428
+ (regression) => signalTrippedInput({
22429
+ criterion_id: regression.criterion_id,
22430
+ surface: regression.surface,
22431
+ signal: regression.signal,
22432
+ detail: regression.detail
22433
+ })
22434
+ ),
22435
+ common
22436
+ );
22437
+ console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
22438
+ `);
22439
+ }
22440
+ process.exit(regressions.length > 0 ? 1 : 0);
22441
+ }
21949
22442
  function runIssue(args, common) {
21950
22443
  const { single, flags, positionals } = collect(args);
21951
22444
  if (flags.has("help")) {
@@ -22085,6 +22578,8 @@ function runKritik(args) {
22085
22578
  return runMatrix(subArgs, common);
22086
22579
  case "signals":
22087
22580
  return runSignals(subArgs, common);
22581
+ case "regressions":
22582
+ return runRegressions(subArgs, common);
22088
22583
  case "issue":
22089
22584
  return runIssue(subArgs, common);
22090
22585
  case "criterion":
@@ -22109,13 +22604,13 @@ Commands:
22109
22604
  release <version> [path] Tag a release (append release.tagged) and draft its notes.
22110
22605
  deliverable <title> [path] Record a deliverable (append deliverable.shipped).
22111
22606
  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).
22607
+ pack [options] [path] Produce a self-contained interchange bundle (embeds journal + quality).
22113
22608
  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.
22609
+ push [options] [path] Validate, pack (journal + quality stripped), and publish to Publik.
22115
22610
  --delete <id> --key <owner_key> removes a snapshot.
22116
22611
  link [options] [path] Point this repo at a hosted project so an agent can edit it.
22117
22612
  --list shows the projects your token can reach.
22118
- restore [options] [path] Replace the linked hosted project's bundle + journal (backs up first).
22613
+ restore [options] [path] Replace the linked project's bundle, journal + quality (backs up first).
22119
22614
  bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
22120
22615
  kritik <sub> [options] Quality audits: score criteria, open findings, roll up the matrix.
22121
22616
 
@@ -22124,7 +22619,7 @@ Options:
22124
22619
  -v, --version Print the version.
22125
22620
 
22126
22621
  Run "arkaik <command> --help" for command-specific help.`;
22127
- var VERSION = "0.2.0";
22622
+ var VERSION = "0.4.0";
22128
22623
  function main(argv) {
22129
22624
  const [command, ...rest] = argv;
22130
22625
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {