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.
- package/dist/index.js +757 -288
- package/dist/io.js +9 -4
- 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,
|
|
18581
|
-
var
|
|
18582
|
-
var
|
|
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)
|
|
18586
|
-
|
|
18587
|
-
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
18678
|
-
if (!
|
|
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 =
|
|
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 =
|
|
18693
|
-
|
|
18694
|
-
|
|
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
|
-
|
|
18727
|
-
|
|
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
|
|
19159
|
+
import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
18753
19160
|
import { tmpdir } from "node:os";
|
|
18754
|
-
import { join as
|
|
18755
|
-
var
|
|
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: ${
|
|
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 =
|
|
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(
|
|
18808
|
-
outPath =
|
|
18809
|
-
|
|
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 {
|
|
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] ??
|
|
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
|
|
18869
|
-
var
|
|
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: ${
|
|
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 =
|
|
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
|
|
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] ??
|
|
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
|
|
19140
|
-
import { dirname as
|
|
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 =
|
|
19212
|
-
const linkPath =
|
|
19213
|
-
|
|
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(
|
|
19660
|
+
existing = JSON.parse(readFileSync8(linkPath, "utf8"));
|
|
19217
19661
|
} catch {
|
|
19218
19662
|
}
|
|
19219
|
-
|
|
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
|
|
19247
|
-
import { join as
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
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 =
|
|
19437
|
-
if (!
|
|
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(
|
|
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 =
|
|
19453
|
-
if (!
|
|
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(
|
|
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 =
|
|
20047
|
+
const backupDir = join8(cwd, "docs", "arkaik", ".backups");
|
|
19530
20048
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19531
|
-
const backupPath =
|
|
20049
|
+
const backupPath = join8(backupDir, `${stamp}-bundle.json`);
|
|
19532
20050
|
const backupContent = `${JSON.stringify(exported, null, 2)}
|
|
19533
20051
|
`;
|
|
19534
20052
|
try {
|
|
19535
|
-
|
|
20053
|
+
mkdirSync6(backupDir, { recursive: true });
|
|
19536
20054
|
writeBackupFile(backupPath, backupContent);
|
|
19537
|
-
JSON.parse(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
19843
|
-
|
|
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
|
-
|
|
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 (!
|
|
19852
|
-
return
|
|
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
|
|
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 (!
|
|
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(
|
|
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
|
|
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,
|
|
20034
|
-
if (!
|
|
20582
|
+
const file2 = at(cwd, PROFILE_FILE2);
|
|
20583
|
+
if (!existsSync13(file2)) return null;
|
|
20035
20584
|
try {
|
|
20036
|
-
return JSON.parse(
|
|
20585
|
+
return JSON.parse(readFileSync13(file2, "utf8"));
|
|
20037
20586
|
} catch (err) {
|
|
20038
|
-
throw new Error(`cannot read ${
|
|
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 (!
|
|
20592
|
+
if (!existsSync13(file2)) return null;
|
|
20044
20593
|
try {
|
|
20045
|
-
return JSON.parse(
|
|
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
|
-
|
|
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 (!
|
|
20607
|
+
if (!existsSync13(file2)) return "greenfield";
|
|
20059
20608
|
let parsed;
|
|
20060
20609
|
try {
|
|
20061
|
-
parsed = JSON.parse(
|
|
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
|
|
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 (!
|
|
20557
|
-
const parsed = JSON.parse(
|
|
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 ${
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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") {
|