snipara-companion 3.0.7 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,7 @@ These commands are useful without hosted Snipara:
43
43
  | ------------------------------------------------------------ | ------------------------------------------------------------ |
44
44
  | `source init` / `source sync` / `source status` | Local source snapshot, document preview, and code overlay |
45
45
  | `impact` / `code impact` | File-level blast-radius from the local code overlay |
46
- | `reality-check` | Intent/verification checks for local or supplied changes |
46
+ | `reality-check` | Intent Ledger, Unknown Registry, and verification checks |
47
47
  | `code callers` / `imports` / `neighbors` / `shortest-path` | Structural repo questions from local files |
48
48
  | `workflow start` / `phase-start` / `phase-commit` / `resume` | Agent continuity that survives compaction |
49
49
  | `context-pack` | Reversible local packs for long logs, diffs, and tool output |
package/dist/index.js CHANGED
@@ -19687,6 +19687,8 @@ var PROJECT_INTELLIGENCE_ENGINEERING_LEAD_PROOF_VERIFICATION_SOURCES = [
19687
19687
  "unknown"
19688
19688
  ];
19689
19689
  var PROJECT_REALITY_CHECK_VERSION = "project-reality-check-v0";
19690
+ var PROJECT_INTENT_LEDGER_VERSION = "intent-ledger-v1";
19691
+ var PROJECT_UNKNOWN_REGISTRY_VERSION = "unknown-registry-v1";
19690
19692
  var SENSITIVE_SURFACES = [
19691
19693
  {
19692
19694
  key: "billing_atomicity",
@@ -19796,6 +19798,444 @@ function decisionText(decision) {
19796
19798
  ...decision.constraints ?? []
19797
19799
  ].filter(Boolean).join(" ");
19798
19800
  }
19801
+ function confidenceBand(score) {
19802
+ if (score >= 0.8) return "high";
19803
+ if (score >= 0.55) return "medium";
19804
+ return "low";
19805
+ }
19806
+ function normalizeConfidenceScore(value, fallback) {
19807
+ if (typeof value !== "number" || Number.isNaN(value)) return fallback;
19808
+ return Math.max(0, Math.min(1, value));
19809
+ }
19810
+ function intentStatusFromDecision(decision) {
19811
+ const status = normalizeText(decision.status);
19812
+ if (/contradict|conflict/.test(status)) return "contradicted";
19813
+ if (/supersed|replac/.test(status)) return "superseded";
19814
+ if (/stale|expired|deprecated/.test(status)) return "stale";
19815
+ if (/approved|accepted|active|confirmed/.test(status)) return "approved";
19816
+ return "review_pending";
19817
+ }
19818
+ function intentConfidence(score, status, reasonCodes) {
19819
+ return {
19820
+ score,
19821
+ band: confidenceBand(score),
19822
+ state: status === "approved" ? "confirmed" : status === "stale" ? "stale" : status === "contradicted" ? "conflicted" : score < 0.55 ? "weak" : "review_pending",
19823
+ reasonCodes
19824
+ };
19825
+ }
19826
+ function stalenessFromStatus(status) {
19827
+ if (status === "stale" || status === "superseded") {
19828
+ return {
19829
+ state: "stale",
19830
+ reason: status === "superseded" ? "The source decision is marked as superseded." : "The source decision is marked as stale."
19831
+ };
19832
+ }
19833
+ if (status === "review_pending") {
19834
+ return {
19835
+ state: "unknown",
19836
+ reason: "The intent has not been reviewed as current project truth."
19837
+ };
19838
+ }
19839
+ return { state: "fresh", horizonDays: 90 };
19840
+ }
19841
+ function anchorMatchesFile(anchor, file) {
19842
+ const normalizedAnchor = anchor.replace(/^\.\//, "").trim();
19843
+ const normalizedFile = file.replace(/^\.\//, "").trim();
19844
+ return normalizedAnchor.length > 0 && normalizedFile.length > 0 && (normalizedFile.includes(normalizedAnchor) || normalizedAnchor.includes(normalizedFile));
19845
+ }
19846
+ function intentEntryMatchesFile(entry, file) {
19847
+ return entry.affectedAnchors.some((anchor) => anchorMatchesFile(anchor, file));
19848
+ }
19849
+ function inferDecisionAnchors(decision, changedFiles) {
19850
+ const explicitAnchors = uniqueStrings8(decision.affectedAnchors ?? []);
19851
+ if (explicitAnchors.length > 0) return explicitAnchors;
19852
+ const text = normalizeText(decisionText(decision));
19853
+ const inferred = SENSITIVE_SURFACES.flatMap(
19854
+ (surface) => matchesAnyText(text, surface.patterns) ? surfaceFiles(surface, changedFiles) : []
19855
+ );
19856
+ return uniqueStrings8(inferred);
19857
+ }
19858
+ function splitIntentPhrases(value) {
19859
+ if (!value) return [];
19860
+ return uniqueStrings8(
19861
+ value.split(/[;\n]/g).map((item) => item.replace(/^[-*]\s*/, "").trim()).filter(Boolean)
19862
+ ).slice(0, 6);
19863
+ }
19864
+ function extractAntiGoals(text) {
19865
+ const matches = [...text.matchAll(/\b(?:avoid|forbid|prevent|do not|don't)\b[^.;\n]*/gi)].map(
19866
+ (match) => match[0].trim()
19867
+ );
19868
+ return uniqueStrings8(matches).slice(0, 5);
19869
+ }
19870
+ function extractRejectedAlternatives(text) {
19871
+ const matches = [
19872
+ ...text.matchAll(/\b(?:rejected|instead of|rather than|not use|not using)\b[^.;\n]*/gi)
19873
+ ].map((match) => match[0].trim());
19874
+ return uniqueStrings8(matches).slice(0, 5);
19875
+ }
19876
+ function documentLooksLikeIntent(doc) {
19877
+ const text = normalizeText([doc.path, doc.title, doc.contentPreview, doc.kind].join(" "));
19878
+ return /\b(adr|decision|intent|rationale|constraint|architecture|runbook)\b/.test(text);
19879
+ }
19880
+ function buildDecisionIntentEntry(decision, changedFiles) {
19881
+ const status = intentStatusFromDecision(decision);
19882
+ const anchors = inferDecisionAnchors(decision, changedFiles);
19883
+ const fullText = decisionText(decision);
19884
+ const confidenceScore = normalizeConfidenceScore(
19885
+ decision.confidenceScore,
19886
+ status === "approved" ? 0.86 : 0.66
19887
+ );
19888
+ const reasonCodes = uniqueStrings8([
19889
+ "decision_intent",
19890
+ status === "approved" ? "reviewed_source" : "review_needed",
19891
+ anchors.length > 0 ? "affected_anchors_present" : "affected_anchors_missing"
19892
+ ]);
19893
+ return {
19894
+ id: `intent:${decision.id}`,
19895
+ title: decision.title,
19896
+ status,
19897
+ source: "decision",
19898
+ sourceDecisionId: decision.id,
19899
+ sourceDocumentPath: null,
19900
+ intent: {
19901
+ goal: decision.rationale || decision.decision || decision.title,
19902
+ constraints: uniqueStrings8([
19903
+ ...decision.constraints ?? [],
19904
+ ...splitIntentPhrases(decision.scope)
19905
+ ]).slice(0, 8),
19906
+ antiGoals: extractAntiGoals(fullText),
19907
+ rejectedAlternatives: extractRejectedAlternatives(fullText)
19908
+ },
19909
+ affectedAnchors: anchors,
19910
+ owner: null,
19911
+ evidence: decision.evidence && decision.evidence.length > 0 ? decision.evidence : [
19912
+ {
19913
+ kind: "decision",
19914
+ label: `${decision.id}: ${decision.title}`,
19915
+ sourceRef: decision.id,
19916
+ strength: confidenceScore
19917
+ }
19918
+ ],
19919
+ confidence: intentConfidence(confidenceScore, status, reasonCodes),
19920
+ staleness: stalenessFromStatus(status),
19921
+ reasonCodes,
19922
+ recommendedActions: status === "approved" ? ["Keep this intent linked when changing its affected anchors."] : ["Review this intent candidate before treating it as governing project truth."],
19923
+ caveats: [
19924
+ "Intent Ledger V1 preserves source-backed intent; it does not infer unstated architecture rules."
19925
+ ]
19926
+ };
19927
+ }
19928
+ function buildDocumentIntentEntry(doc) {
19929
+ const preview4 = doc.contentPreview || doc.title || doc.path;
19930
+ const reasonCodes = ["document_intent_candidate", "review_needed"];
19931
+ const confidenceScore = doc.kind && /adr|decision/i.test(doc.kind) ? 0.66 : 0.58;
19932
+ return {
19933
+ id: `intent:doc:${doc.path}`,
19934
+ title: doc.title || doc.path,
19935
+ status: "review_pending",
19936
+ source: "document",
19937
+ sourceDecisionId: null,
19938
+ sourceDocumentPath: doc.path,
19939
+ intent: {
19940
+ goal: preview4,
19941
+ constraints: splitIntentPhrases(doc.contentPreview).slice(0, 4),
19942
+ antiGoals: extractAntiGoals(preview4),
19943
+ rejectedAlternatives: extractRejectedAlternatives(preview4)
19944
+ },
19945
+ affectedAnchors: [doc.path],
19946
+ owner: null,
19947
+ evidence: [
19948
+ {
19949
+ kind: "document",
19950
+ label: doc.title ? `${doc.path}: ${doc.title}` : doc.path,
19951
+ sourceRef: doc.sourceRef ?? doc.path,
19952
+ strength: confidenceScore
19953
+ }
19954
+ ],
19955
+ confidence: intentConfidence(confidenceScore, "review_pending", reasonCodes),
19956
+ staleness: {
19957
+ state: "unknown",
19958
+ reason: "Document-derived intent candidates need review before becoming governing truth."
19959
+ },
19960
+ reasonCodes,
19961
+ recommendedActions: ["Promote, reject, or link this document intent candidate to a decision."],
19962
+ caveats: [
19963
+ "Document candidates are weaker than reviewed decisions until authority is confirmed."
19964
+ ]
19965
+ };
19966
+ }
19967
+ function buildProjectIntentLedger(input, changedFilesOverride) {
19968
+ const changedFiles = uniqueStrings8(changedFilesOverride ?? input.changedFiles);
19969
+ const decisionEntries = (input.decisions ?? []).map(
19970
+ (decision) => buildDecisionIntentEntry(decision, changedFiles)
19971
+ );
19972
+ const decisionDocumentPaths = new Set(
19973
+ decisionEntries.flatMap(
19974
+ (entry) => entry.evidence.map((evidence) => evidence.sourceRef).filter(Boolean)
19975
+ )
19976
+ );
19977
+ const documentEntries = (input.documents ?? []).filter(documentLooksLikeIntent).filter((doc) => !decisionDocumentPaths.has(doc.path)).map(buildDocumentIntentEntry);
19978
+ const entries = [...decisionEntries, ...documentEntries];
19979
+ const linkedIntentIds = new Set(
19980
+ entries.filter((entry) => changedFiles.some((file) => intentEntryMatchesFile(entry, file))).map((entry) => entry.id)
19981
+ );
19982
+ const missingAnchors = changedFiles.filter(
19983
+ (file) => !entries.some((entry) => intentEntryMatchesFile(entry, file))
19984
+ );
19985
+ const coverage = changedFiles.length === 0 || entries.length > 0 && missingAnchors.length === 0 ? "linked" : entries.length === 0 ? "missing" : "partial";
19986
+ const reasonCodes = uniqueStrings8([
19987
+ coverage === "linked" ? "intent_coverage_linked" : void 0,
19988
+ coverage === "partial" ? "intent_coverage_partial" : void 0,
19989
+ coverage === "missing" ? "intent_coverage_missing" : void 0,
19990
+ entries.some((entry) => entry.status === "review_pending") ? "intent_review_pending" : void 0,
19991
+ entries.some((entry) => entry.staleness.state !== "fresh") ? "intent_freshness_unknown" : void 0
19992
+ ]);
19993
+ return {
19994
+ version: PROJECT_INTENT_LEDGER_VERSION,
19995
+ coverage,
19996
+ totalIntentCount: entries.length,
19997
+ linkedIntentCount: linkedIntentIds.size,
19998
+ missingAnchorCount: missingAnchors.length,
19999
+ entries: entries.slice(0, 20),
20000
+ missingAnchors: missingAnchors.slice(0, 20),
20001
+ evidence: entries.flatMap((entry) => entry.evidence).slice(0, 10),
20002
+ reasonCodes,
20003
+ caveats: [
20004
+ "Intent Ledger V1 is a deterministic projection from supplied decisions and documents.",
20005
+ coverage === "missing" ? "Missing intent means no linked source was supplied for this scope, not that no governing intent exists." : void 0
20006
+ ].filter((item) => Boolean(item))
20007
+ };
20008
+ }
20009
+ function unknownId(category, key, scope) {
20010
+ return makeFindingId("decision_vs_code", `${category}:${key}`, scope).replace(
20011
+ "reality:decision-vs-code",
20012
+ "unknown"
20013
+ );
20014
+ }
20015
+ function unknownStatusFromSeverity(unknowns) {
20016
+ if (unknowns.some(
20017
+ (unknown) => unknown.severity === "blocking" || unknown.severity === "review_required"
20018
+ )) {
20019
+ return "risk";
20020
+ }
20021
+ if (unknowns.some((unknown) => unknown.severity === "watch")) return "watch";
20022
+ return "clear";
20023
+ }
20024
+ function unknownConfidence(score, state, reasonCodes) {
20025
+ return {
20026
+ score,
20027
+ band: confidenceBand(score),
20028
+ state,
20029
+ reasonCodes
20030
+ };
20031
+ }
20032
+ function makeUnknown(input) {
20033
+ return {
20034
+ id: unknownId(input.category, input.title, input.scope),
20035
+ category: input.category,
20036
+ severity: input.severity,
20037
+ title: input.title,
20038
+ summary: input.summary,
20039
+ scope: input.scope.slice(0, 12),
20040
+ ownerCandidate: input.ownerCandidate ?? null,
20041
+ confidence: unknownConfidence(
20042
+ input.confidenceScore,
20043
+ input.confidenceState ?? "review_pending",
20044
+ input.reasonCodes
20045
+ ),
20046
+ linkedFindingIds: input.linkedFindingIds ?? [],
20047
+ linkedIntentIds: input.linkedIntentIds ?? [],
20048
+ evidence: (input.evidence ?? []).slice(0, 8),
20049
+ reasonCodes: input.reasonCodes,
20050
+ resolutionActions: input.resolutionActions,
20051
+ closureEvidenceRequired: input.closureEvidenceRequired,
20052
+ caveats: input.caveats ?? []
20053
+ };
20054
+ }
20055
+ function buildProjectUnknownRegistry(input) {
20056
+ const unknowns = [];
20057
+ if (input.intentLedger.missingAnchors.length > 0) {
20058
+ unknowns.push(
20059
+ makeUnknown({
20060
+ category: "missing_intent",
20061
+ severity: input.findings.some((finding) => finding.severity === "review_required") ? "review_required" : "watch",
20062
+ title: "Governing intent is not linked for part of this scope",
20063
+ summary: "At least one changed anchor has no linked reviewed decision or document intent. This is an unknown, not proof that no constraint exists.",
20064
+ scope: input.intentLedger.missingAnchors,
20065
+ confidenceScore: 0.86,
20066
+ linkedFindingIds: input.findings.filter((finding) => finding.reasonCodes.includes("missing_intent")).map((finding) => finding.id),
20067
+ evidence: input.intentLedger.evidence,
20068
+ reasonCodes: ["missing_intent", "unknown_governing_decision"],
20069
+ resolutionActions: [
20070
+ "Link an existing decision, approve a review-pending intent, or create a new decision candidate."
20071
+ ],
20072
+ closureEvidenceRequired: [
20073
+ "Reviewed decision, ADR, issue, PR Answer Pack, or final workflow commit linked to the changed anchor."
20074
+ ],
20075
+ caveats: [
20076
+ "Retrieval/index freshness can make linked intent appear missing until context is refreshed."
20077
+ ]
20078
+ })
20079
+ );
20080
+ }
20081
+ for (const entry of input.intentLedger.entries) {
20082
+ if (entry.status === "review_pending") {
20083
+ unknowns.push(
20084
+ makeUnknown({
20085
+ category: "intent_review_pending",
20086
+ severity: "watch",
20087
+ title: `Intent needs review: ${entry.title}`,
20088
+ summary: "A source-backed intent candidate exists, but it is not reviewed enough to act as governing project truth.",
20089
+ scope: entry.affectedAnchors,
20090
+ confidenceScore: entry.confidence.score,
20091
+ linkedIntentIds: [entry.id],
20092
+ evidence: entry.evidence,
20093
+ reasonCodes: ["intent_review_pending"],
20094
+ resolutionActions: ["Approve, reject, supersede, or link this intent candidate."],
20095
+ closureEvidenceRequired: ["Review receipt or approved ProjectDecision authority state."],
20096
+ caveats: entry.caveats
20097
+ })
20098
+ );
20099
+ }
20100
+ if (entry.status === "stale" || entry.status === "superseded") {
20101
+ unknowns.push(
20102
+ makeUnknown({
20103
+ category: "stale_intent",
20104
+ severity: "review_required",
20105
+ title: `Intent may be stale: ${entry.title}`,
20106
+ summary: "A linked intent is stale or superseded; current implementation may no longer match the original rationale.",
20107
+ scope: entry.affectedAnchors,
20108
+ confidenceScore: 0.78,
20109
+ confidenceState: "stale",
20110
+ linkedIntentIds: [entry.id],
20111
+ evidence: entry.evidence,
20112
+ reasonCodes: ["stale_intent", entry.status],
20113
+ resolutionActions: [
20114
+ "Refresh, supersede, or archive the stale intent before relying on it."
20115
+ ],
20116
+ closureEvidenceRequired: ["Updated decision or explicit supersession evidence."],
20117
+ caveats: entry.caveats
20118
+ })
20119
+ );
20120
+ }
20121
+ }
20122
+ for (const finding of input.findings) {
20123
+ if (finding.reasonCodes.includes("verification_missing")) {
20124
+ unknowns.push(
20125
+ makeUnknown({
20126
+ category: "missing_verification",
20127
+ severity: finding.severity,
20128
+ title: `Verification gap: ${finding.title}`,
20129
+ summary: "The change touches a sensitive surface but no matching verification evidence was supplied.",
20130
+ scope: finding.changedFiles,
20131
+ confidenceScore: 0.82,
20132
+ linkedFindingIds: [finding.id],
20133
+ linkedIntentIds: finding.decisionIds.map((id) => `intent:${id}`),
20134
+ evidence: finding.evidence,
20135
+ reasonCodes: ["missing_verification", ...finding.reasonCodes],
20136
+ resolutionActions: finding.recommendedActions,
20137
+ closureEvidenceRequired: [
20138
+ "Focused test, smoke check, review proof, or verification checklist item linked to the changed behavior."
20139
+ ],
20140
+ caveats: finding.caveats
20141
+ })
20142
+ );
20143
+ }
20144
+ if (finding.type === "architecture_drift") {
20145
+ unknowns.push(
20146
+ makeUnknown({
20147
+ category: "architecture_drift",
20148
+ severity: finding.severity,
20149
+ title: finding.title,
20150
+ summary: "A boundary-spanning change needs architecture-intent confirmation before treating the current dependency shape as intended.",
20151
+ scope: finding.changedFiles,
20152
+ confidenceScore: 0.72,
20153
+ linkedFindingIds: [finding.id],
20154
+ evidence: finding.evidence,
20155
+ reasonCodes: ["architecture_drift_unknown", ...finding.reasonCodes],
20156
+ resolutionActions: finding.recommendedActions,
20157
+ closureEvidenceRequired: [
20158
+ "Architecture rule, reviewed decision, or code graph evidence proving the boundary is intentional."
20159
+ ],
20160
+ caveats: finding.caveats
20161
+ })
20162
+ );
20163
+ }
20164
+ if (finding.reasonCodes.includes("risk_term_in_diff") || finding.reasonCodes.includes("no_linked_decision") && finding.severity === "review_required") {
20165
+ unknowns.push(
20166
+ makeUnknown({
20167
+ category: "heuristic_calibration",
20168
+ severity: "watch",
20169
+ title: `Heuristic signal needs calibration: ${finding.title}`,
20170
+ summary: "Reality Check V1 used regex/path/free-text signals for this finding. Keep it advisory or narrow-scope until project-specific thresholds are calibrated.",
20171
+ scope: finding.changedFiles,
20172
+ confidenceScore: 0.68,
20173
+ confidenceState: "weak",
20174
+ linkedFindingIds: [finding.id],
20175
+ evidence: finding.evidence,
20176
+ reasonCodes: ["heuristic_calibration_needed", ...finding.reasonCodes],
20177
+ resolutionActions: [
20178
+ "Confirm the finding against structured intent, tests, or reviewed project-specific rules before broad CI gating."
20179
+ ],
20180
+ closureEvidenceRequired: [
20181
+ "Reviewed calibration decision or false-positive/true-positive outcome history for this surface."
20182
+ ],
20183
+ caveats: [
20184
+ "--enforce should remain opt-in for this finding family until calibration evidence exists."
20185
+ ]
20186
+ })
20187
+ );
20188
+ }
20189
+ }
20190
+ if (input.dirtyFiles.length > 0) {
20191
+ unknowns.push(
20192
+ makeUnknown({
20193
+ category: "dirty_local_evidence",
20194
+ severity: "watch",
20195
+ title: "Local dirty evidence is not hosted truth",
20196
+ summary: "The local scan included dirty files that hosted PR checks and code graph indexes cannot see until committed.",
20197
+ scope: input.dirtyFiles,
20198
+ confidenceScore: 0.9,
20199
+ linkedFindingIds: input.findings.filter((finding) => finding.reasonCodes.includes("dirty_working_tree")).map((finding) => finding.id),
20200
+ evidence: input.dirtyFiles.map((file) => ({
20201
+ kind: "repository",
20202
+ label: file,
20203
+ sourceRef: file,
20204
+ strength: 0.5
20205
+ })),
20206
+ reasonCodes: ["dirty_local_evidence", "hosted_context_gap"],
20207
+ resolutionActions: [
20208
+ "Commit, stash, or exclude dirty files before relying on hosted checks."
20209
+ ],
20210
+ closureEvidenceRequired: ["Clean git status or explicit review of dirty files."],
20211
+ caveats: ["Dirty-file unknowns are local-only and should not be treated as PR evidence."]
20212
+ })
20213
+ );
20214
+ }
20215
+ const deduped = Array.from(
20216
+ new Map(unknowns.map((unknown) => [unknown.id, unknown])).values()
20217
+ ).slice(0, 24);
20218
+ const status = unknownStatusFromSeverity(deduped);
20219
+ return {
20220
+ version: PROJECT_UNKNOWN_REGISTRY_VERSION,
20221
+ status,
20222
+ unknownCount: deduped.length,
20223
+ riskCount: deduped.filter(
20224
+ (unknown) => unknown.severity === "review_required" || unknown.severity === "blocking"
20225
+ ).length,
20226
+ watchCount: deduped.filter((unknown) => unknown.severity === "watch").length,
20227
+ categories: uniqueStrings8(
20228
+ deduped.map((unknown) => unknown.category)
20229
+ ),
20230
+ unknowns: deduped,
20231
+ evidence: deduped.flatMap((unknown) => unknown.evidence).slice(0, 12),
20232
+ reasonCodes: uniqueStrings8(deduped.flatMap((unknown) => unknown.reasonCodes)).slice(0, 24),
20233
+ caveats: [
20234
+ "Unknown Registry V1 records evidence-backed gaps; closing an unknown requires source-backed evidence.",
20235
+ deduped.some((unknown) => unknown.category === "heuristic_calibration") ? "Heuristic calibration unknowns prevent regex/path findings from masquerading as fully calibrated truth." : void 0
20236
+ ].filter((item) => Boolean(item))
20237
+ };
20238
+ }
19799
20239
  function surfaceFiles(surface, changedFiles) {
19800
20240
  return changedFiles.filter((file) => surface.patterns.some((pattern) => pattern.test(file)));
19801
20241
  }
@@ -19836,6 +20276,7 @@ function buildProjectRealityCheck(input) {
19836
20276
  const findings = [];
19837
20277
  const decisions = input.decisions ?? [];
19838
20278
  const contextEvidence2 = [...docEvidence(input), ...symbolEvidence(input)];
20279
+ const intentLedger = buildProjectIntentLedger(input, changedFiles);
19839
20280
  for (const surface of SENSITIVE_SURFACES) {
19840
20281
  const files = surfaceFiles(surface, changedFiles);
19841
20282
  if (files.length === 0) continue;
@@ -19896,7 +20337,7 @@ function buildProjectRealityCheck(input) {
19896
20337
  ]
19897
20338
  });
19898
20339
  }
19899
- if (changedFiles.length > 0 && decisions.length === 0 && (input.source === "pull_request" || findings.length > 0)) {
20340
+ if (changedFiles.length > 0 && intentLedger.linkedIntentCount === 0 && (input.source === "pull_request" || findings.length > 0)) {
19900
20341
  findings.push({
19901
20342
  id: makeFindingId("decision_vs_code", "missing_intent", changedFiles),
19902
20343
  type: "decision_vs_code",
@@ -19961,6 +20402,13 @@ function buildProjectRealityCheck(input) {
19961
20402
  0,
19962
20403
  20
19963
20404
  );
20405
+ const unknownRegistry = buildProjectUnknownRegistry({
20406
+ realityCheck: input,
20407
+ changedFiles,
20408
+ dirtyFiles,
20409
+ findings,
20410
+ intentLedger
20411
+ });
19964
20412
  const caveats = uniqueStrings8([
19965
20413
  "Project Reality Check V0 is advisory and evidence-backed; it is not deterministic repository simulation.",
19966
20414
  input.source === "local" ? "Local results may include dirty files that hosted PR checks cannot see." : "PR results are scoped to GitHub-provided PR files and indexed Snipara context."
@@ -19975,9 +20423,15 @@ function buildProjectRealityCheck(input) {
19975
20423
  changedFileCount: changedFiles.length,
19976
20424
  findingCount: findings.length,
19977
20425
  findings,
20426
+ intentLedger,
20427
+ unknownRegistry,
19978
20428
  requiredActions,
19979
20429
  watchItems,
19980
- reasonCodes,
20430
+ reasonCodes: uniqueStrings8([
20431
+ ...reasonCodes,
20432
+ ...intentLedger.reasonCodes,
20433
+ ...unknownRegistry.reasonCodes
20434
+ ]).slice(0, 30),
19981
20435
  caveats
19982
20436
  };
19983
20437
  }
@@ -19987,6 +20441,45 @@ function renderProjectRealityCheckMarkdown(result) {
19987
20441
  `- Scope: ${result.changedFileCount} changed file(s), ${result.findingCount} finding(s).`,
19988
20442
  `- Summary: ${result.summary}`
19989
20443
  ];
20444
+ lines.push(
20445
+ "",
20446
+ "Intent Ledger:",
20447
+ `- Coverage: ${result.intentLedger.coverage}; ${result.intentLedger.linkedIntentCount}/${result.intentLedger.totalIntentCount} intent(s) linked to this scope.`
20448
+ );
20449
+ for (const entry of result.intentLedger.entries.slice(0, 5)) {
20450
+ lines.push(
20451
+ `- ${entry.status.toUpperCase()} ${entry.id}: ${entry.title}`,
20452
+ ` Intent: ${entry.intent.goal}`,
20453
+ ` Confidence: ${entry.confidence.band} (${Math.round(entry.confidence.score * 100)}%).`
20454
+ );
20455
+ if (entry.affectedAnchors.length > 0) {
20456
+ lines.push(` Anchors: ${entry.affectedAnchors.slice(0, 5).join(", ")}`);
20457
+ }
20458
+ }
20459
+ if (result.intentLedger.missingAnchors.length > 0) {
20460
+ lines.push(
20461
+ `- Missing linked intent for: ${result.intentLedger.missingAnchors.slice(0, 8).map((file) => `\`${file}\``).join(", ")}`
20462
+ );
20463
+ }
20464
+ lines.push(
20465
+ "",
20466
+ "Unknown Registry:",
20467
+ `- Status: ${result.unknownRegistry.status}; ${result.unknownRegistry.unknownCount} unknown(s), ${result.unknownRegistry.riskCount} risk item(s).`
20468
+ );
20469
+ for (const unknown of result.unknownRegistry.unknowns.slice(0, 6)) {
20470
+ lines.push(
20471
+ `- ${unknown.severity.toUpperCase()} ${unknown.category}: ${unknown.title}`,
20472
+ ` ${unknown.summary}`
20473
+ );
20474
+ if (unknown.scope.length > 0) {
20475
+ lines.push(
20476
+ ` Scope: ${unknown.scope.slice(0, 5).map((item) => `\`${item}\``).join(", ")}`
20477
+ );
20478
+ }
20479
+ if (unknown.resolutionActions.length > 0) {
20480
+ lines.push(` Resolve: ${unknown.resolutionActions[0]}`);
20481
+ }
20482
+ }
19990
20483
  if (result.findings.length > 0) {
19991
20484
  lines.push("", "Findings:");
19992
20485
  for (const finding of result.findings.slice(0, 8)) {
@@ -195,8 +195,13 @@ snipara-companion workflow resume --include-session-context
195
195
  - `brief` is the short alias for `intelligence brief`.
196
196
  - `reality-check` compares changed files, linked intent, context docs, symbols,
197
197
  and verification hints to flag contradiction-to-reality risks before commit or
198
- merge. Use `--enforce` in local hooks when review-required findings should
199
- fail the hook.
198
+ merge. Output includes an Intent Ledger section with source-backed intent
199
+ coverage, confidence, affected anchors, and missing intent anchors, plus an
200
+ Unknown Registry section that ranks missing intent, missing verification,
201
+ dirty local evidence, architecture drift, stale/review-pending intent, and
202
+ heuristic-calibration gaps. Keep `--enforce` opt-in for narrow hooks or
203
+ calibrated CI adapters; the V1 heuristics are advisory and can produce false
204
+ positives on broad text/path matches.
200
205
  - `timeline` is the Git-style log for workflow starts, phase starts, phase
201
206
  commits, final commits, and Team Sync handoffs.
202
207
  - `workflow impact-gate` audits committed local workflow phases that are ahead
@@ -951,7 +956,9 @@ snipara-companion reality-check \
951
956
  The command reads the local Git scope by default, includes dirty files unless
952
957
  `--no-include-dirty` is set, and also accepts explicit `--changed-files` for
953
958
  hooks or CI collectors. `--enforce` exits non-zero for `review_required` or
954
- `blocking` findings; JSON output is available with `--json`.
959
+ `blocking` findings, but should stay opt-in until the matched surfaces,
960
+ verification signals, and project-specific thresholds have been calibrated.
961
+ JSON output is available with `--json`.
955
962
 
956
963
  Use `intelligence ledger-export` when an agent run, review, or replay benchmark
957
964
  needs a portable Coding Intelligence Ledger instead of a raw transcript:
@@ -1028,7 +1035,7 @@ Semantics:
1028
1035
  - `snipara-companion status` = top-level agentic work status across local workflow state, git dirtiness, and Team Sync carryover
1029
1036
  - `snipara-companion source init|sync|status|snapshot|watch` = automatic local source activation for folders with or without Git metadata; writes `.snipara/source/latest.json`, previews document sync, and refreshes the local code overlay cache
1030
1037
  - `snipara-companion brief` = short alias for `snipara-companion intelligence brief`
1031
- - `snipara-companion reality-check` = local Project Reality Check for supplied or Git-derived changed files; `--enforce` makes review-required contradiction-to-reality findings fail hooks
1038
+ - `snipara-companion reality-check` = local Project Reality Check plus Intent Ledger and Unknown Registry coverage for supplied or Git-derived changed files; `--enforce` is an opt-in strict mode for calibrated hooks
1032
1039
  - `snipara-companion timeline` = local timeline of workflow starts, phase starts, phase commits, final commits, and Team Sync handoffs
1033
1040
  - `snipara-companion handoff` = top-level agent-ready Markdown/JSON handoff artifact plus the same local/hosted Team Sync handoff persistence
1034
1041
  - `snipara-companion intelligence brief` = one local Project Intelligence brief that combines resume context, memory health, and code impact for a task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.0.7",
3
+ "version": "3.0.8",
4
4
  "description": "Local-first CLI that asks your repo what breaks before an AI coding agent edits it.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {