snipara-companion 3.0.9 → 3.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9750,15 +9750,15 @@ function parseEnvLine(line) {
9750
9750
  return null;
9751
9751
  }
9752
9752
  const withoutExport = withoutWhitespace.startsWith("export ") ? withoutWhitespace.slice("export ".length).trim() : withoutWhitespace;
9753
- const separatorIndex = withoutExport.indexOf("=");
9754
- if (separatorIndex <= 0) {
9753
+ const separatorIndex2 = withoutExport.indexOf("=");
9754
+ if (separatorIndex2 <= 0) {
9755
9755
  return null;
9756
9756
  }
9757
- const key = withoutExport.slice(0, separatorIndex).trim();
9757
+ const key = withoutExport.slice(0, separatorIndex2).trim();
9758
9758
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
9759
9759
  return null;
9760
9760
  }
9761
- const value = parseEnvValue(withoutExport.slice(separatorIndex + 1).trim());
9761
+ const value = parseEnvValue(withoutExport.slice(separatorIndex2 + 1).trim());
9762
9762
  return { key, value };
9763
9763
  }
9764
9764
  function parseEnvValue(rawValue) {
@@ -19782,7 +19782,15 @@ function decisionText(decision) {
19782
19782
  decision.decision,
19783
19783
  decision.rationale,
19784
19784
  decision.scope,
19785
- ...decision.constraints ?? []
19785
+ decision.owner,
19786
+ ...decision.constraints ?? [],
19787
+ ...decision.antiGoals ?? [],
19788
+ ...decision.rejectedAlternatives ?? [],
19789
+ decision.intent?.goal,
19790
+ decision.intent?.owner,
19791
+ ...decision.intent?.constraints ?? [],
19792
+ ...decision.intent?.antiGoals ?? [],
19793
+ ...decision.intent?.rejectedAlternatives ?? []
19786
19794
  ].filter(Boolean).join(" ");
19787
19795
  }
19788
19796
  function confidenceBand(score) {
@@ -19805,26 +19813,73 @@ function surfaceFiles(surface, changedFiles) {
19805
19813
 
19806
19814
  // ../project-intelligence-contracts/src/intent-ledger.ts
19807
19815
  var PROJECT_INTENT_LEDGER_VERSION = "intent-ledger-v1";
19808
- var PROJECT_INTENT_LEDGER_FRESHNESS_HORIZON_DAYS = 90;
19809
- var DECISION_STATUS_RULES = [
19810
- { status: "contradicted", patterns: [/contradict/i, /conflict/i] },
19811
- { status: "superseded", patterns: [/supersed/i, /replac/i] },
19812
- { status: "stale", patterns: [/stale/i, /expired/i, /deprecated/i] },
19813
- { status: "approved", patterns: [/approved/i, /accepted/i, /active/i, /confirmed/i] }
19814
- ];
19815
- var INTENT_PHRASE_SPLIT_PATTERN = /[;\n]/g;
19816
- var INTENT_MARKDOWN_LIST_MARKER_PATTERN = /^[-*]\s*/;
19817
- var INTENT_NUMBERED_LIST_MARKER_PATTERN = /^\d+\.\s*/;
19818
- var INTENT_TEXT_EXTRACTORS = {
19819
- antiGoals: [/\b(?:avoid|forbid|prevent|do not|don't)\b[^.;\n]*/gi],
19820
- rejectedAlternatives: [/\b(?:rejected|instead of|rather than|not use|not using)\b[^.;\n]*/gi]
19816
+ var PROJECT_INTENT_LEDGER_DEFAULT_FRESHNESS_HORIZON_DAYS = 90;
19817
+ var DEFAULT_PROJECT_INTENT_LEDGER_POLICY = {
19818
+ freshnessHorizonDays: PROJECT_INTENT_LEDGER_DEFAULT_FRESHNESS_HORIZON_DAYS
19821
19819
  };
19822
- function intentStatusFromDecision(decision) {
19823
- const status = normalizeText(decision.status);
19824
- const matchedRule = DECISION_STATUS_RULES.find(
19825
- (rule) => rule.patterns.some((pattern) => pattern.test(status))
19826
- );
19827
- return matchedRule?.status ?? "review_pending";
19820
+ var DECISION_STATUS_ALIASES = {
19821
+ approved: ["approved", "accepted", "active", "confirmed"],
19822
+ review_pending: ["review_pending", "pending_review", "pending", "candidate", "draft"],
19823
+ stale: ["stale", "expired", "deprecated"],
19824
+ superseded: ["superseded", "replaced", "overridden"],
19825
+ contradicted: ["contradicted", "conflicted", "conflict"]
19826
+ };
19827
+ var INTENT_SECTION_ALIASES = {
19828
+ goal: ["goal", "intent", "why", "purpose", "rationale"],
19829
+ constraints: ["constraint", "constraints", "rule", "rules", "must"],
19830
+ antiGoals: ["anti_goal", "anti_goals", "avoid", "forbidden", "do_not"],
19831
+ rejectedAlternatives: [
19832
+ "rejected_alternative",
19833
+ "rejected_alternatives",
19834
+ "rejected",
19835
+ "not_chosen",
19836
+ "alternative_rejected",
19837
+ "alternatives_rejected"
19838
+ ],
19839
+ owner: ["owner", "owners"],
19840
+ freshnessHorizonDays: ["freshness_horizon_days", "horizon_days", "staleness_horizon_days"]
19841
+ };
19842
+ var INTENT_DOCUMENT_KIND_TOKENS = [
19843
+ "adr",
19844
+ "decision",
19845
+ "intent",
19846
+ "rationale",
19847
+ "constraint",
19848
+ "architecture",
19849
+ "runbook"
19850
+ ];
19851
+ function normalizeIdentifier(value) {
19852
+ const normalized = [];
19853
+ let previousWasSeparator = false;
19854
+ for (const char of value?.trim().toLowerCase() ?? "") {
19855
+ const code2 = char.charCodeAt(0);
19856
+ const isAlphaNumeric = code2 >= 48 && code2 <= 57 || code2 >= 97 && code2 <= 122;
19857
+ if (isAlphaNumeric) {
19858
+ normalized.push(char);
19859
+ previousWasSeparator = false;
19860
+ continue;
19861
+ }
19862
+ if (!previousWasSeparator && normalized.length > 0) {
19863
+ normalized.push("_");
19864
+ previousWasSeparator = true;
19865
+ }
19866
+ }
19867
+ if (normalized[normalized.length - 1] === "_") normalized.pop();
19868
+ return normalized.join("");
19869
+ }
19870
+ function statusFromDecisionStatus(decision) {
19871
+ const status = normalizeIdentifier(decision.status);
19872
+ if (!status) return "review_pending";
19873
+ for (const [ledgerStatus, aliases] of Object.entries(DECISION_STATUS_ALIASES)) {
19874
+ if (aliases.includes(status)) return ledgerStatus;
19875
+ }
19876
+ const statusParts = status.split("_").filter(Boolean);
19877
+ for (const [ledgerStatus, aliases] of Object.entries(DECISION_STATUS_ALIASES)) {
19878
+ if (aliases.some((alias) => statusParts.includes(alias))) {
19879
+ return ledgerStatus;
19880
+ }
19881
+ }
19882
+ return "review_pending";
19828
19883
  }
19829
19884
  function intentConfidence(score, status, reasonCodes) {
19830
19885
  return {
@@ -19834,7 +19889,14 @@ function intentConfidence(score, status, reasonCodes) {
19834
19889
  reasonCodes
19835
19890
  };
19836
19891
  }
19837
- function stalenessFromStatus(status) {
19892
+ function normalizeFreshnessHorizonDays(value) {
19893
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
19894
+ return Math.round(value);
19895
+ }
19896
+ function resolveFreshnessHorizonDays(specificValue, policy) {
19897
+ return normalizeFreshnessHorizonDays(specificValue) ?? normalizeFreshnessHorizonDays(policy.freshnessHorizonDays) ?? null;
19898
+ }
19899
+ function stalenessFromStatus(status, freshnessHorizonDays) {
19838
19900
  if (status === "stale" || status === "superseded") {
19839
19901
  return {
19840
19902
  state: "stale",
@@ -19847,7 +19909,11 @@ function stalenessFromStatus(status) {
19847
19909
  reason: "The intent has not been reviewed as current project truth."
19848
19910
  };
19849
19911
  }
19850
- return { state: "fresh", horizonDays: PROJECT_INTENT_LEDGER_FRESHNESS_HORIZON_DAYS };
19912
+ return {
19913
+ state: "fresh",
19914
+ horizonDays: freshnessHorizonDays,
19915
+ reason: freshnessHorizonDays === null ? "No freshness horizon was supplied by source metadata or policy." : null
19916
+ };
19851
19917
  }
19852
19918
  function intentEntryMatchesFile(entry, file) {
19853
19919
  return entry.affectedAnchors.some((anchor) => anchorMatchesFile(anchor, file));
@@ -19862,42 +19928,149 @@ function inferDecisionAnchors(decision, changedFiles) {
19862
19928
  return uniqueStrings8(inferred);
19863
19929
  }
19864
19930
  function normalizeIntentPhrase(value) {
19865
- return value.replace(INTENT_MARKDOWN_LIST_MARKER_PATTERN, "").replace(INTENT_NUMBERED_LIST_MARKER_PATTERN, "").trim();
19931
+ let normalized = value.trim();
19932
+ if (normalized.startsWith("- ") || normalized.startsWith("* ")) {
19933
+ normalized = normalized.slice(2).trim();
19934
+ }
19935
+ const dotIndex = normalized.indexOf(". ");
19936
+ if (dotIndex > 0) {
19937
+ const prefix = normalized.slice(0, dotIndex);
19938
+ if ([...prefix].every((char) => char >= "0" && char <= "9")) {
19939
+ normalized = normalized.slice(dotIndex + 2).trim();
19940
+ }
19941
+ }
19942
+ return normalized;
19866
19943
  }
19867
- function splitIntentPhrases(value) {
19944
+ function splitExplicitIntentList(value) {
19868
19945
  if (!value) return [];
19869
- return uniqueStrings8(
19870
- value.split(INTENT_PHRASE_SPLIT_PATTERN).map(normalizeIntentPhrase).filter(Boolean)
19871
- ).slice(0, 6);
19946
+ return uniqueStrings8(value.split(";").map(normalizeIntentPhrase).filter(Boolean)).slice(0, 6);
19872
19947
  }
19873
- function extractTextMatches(text, patterns, limit) {
19874
- const matches = patterns.flatMap(
19875
- (pattern) => [...text.matchAll(pattern)].map((match) => match[0].trim())
19948
+ function normalizeIntentList(values) {
19949
+ return uniqueStrings8(values.flatMap((value) => splitExplicitIntentList(value))).slice(0, 8);
19950
+ }
19951
+ function emptyParsedIntentSections() {
19952
+ return {
19953
+ constraints: [],
19954
+ antiGoals: [],
19955
+ rejectedAlternatives: [],
19956
+ sourceFields: []
19957
+ };
19958
+ }
19959
+ function intentSectionKey(label) {
19960
+ const normalized = normalizeIdentifier(label);
19961
+ for (const [key, aliases] of Object.entries(INTENT_SECTION_ALIASES)) {
19962
+ if (aliases.includes(normalized)) {
19963
+ return key;
19964
+ }
19965
+ }
19966
+ return void 0;
19967
+ }
19968
+ function separatorIndex(value) {
19969
+ const colon = value.indexOf(":");
19970
+ const equals = value.indexOf("=");
19971
+ if (colon < 0) return equals;
19972
+ if (equals < 0) return colon;
19973
+ return Math.min(colon, equals);
19974
+ }
19975
+ function addSectionValue(parsed, key, value) {
19976
+ const normalizedValue = normalizeIntentPhrase(value);
19977
+ if (!normalizedValue) return;
19978
+ parsed.sourceFields.push(key);
19979
+ if (key === "goal") {
19980
+ parsed.goal = parsed.goal ?? normalizedValue;
19981
+ return;
19982
+ }
19983
+ if (key === "owner") {
19984
+ parsed.owner = parsed.owner ?? normalizedValue;
19985
+ return;
19986
+ }
19987
+ if (key === "freshnessHorizonDays") {
19988
+ parsed.freshnessHorizonDays = parsed.freshnessHorizonDays ?? normalizeFreshnessHorizonDays(Number(normalizedValue));
19989
+ return;
19990
+ }
19991
+ parsed[key].push(...splitExplicitIntentList(normalizedValue));
19992
+ }
19993
+ function parseIntentSections(value) {
19994
+ const parsed = emptyParsedIntentSections();
19995
+ if (!value) return parsed;
19996
+ let currentKey;
19997
+ const lines = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
19998
+ for (const rawLine of lines) {
19999
+ const line = normalizeIntentPhrase(rawLine);
20000
+ if (!line) continue;
20001
+ const splitAt = separatorIndex(line);
20002
+ if (splitAt > 0) {
20003
+ const maybeKey = intentSectionKey(line.slice(0, splitAt));
20004
+ if (maybeKey) {
20005
+ currentKey = maybeKey;
20006
+ addSectionValue(parsed, maybeKey, line.slice(splitAt + 1));
20007
+ continue;
20008
+ }
20009
+ }
20010
+ if (currentKey && (rawLine.trim().startsWith("- ") || rawLine.trim().startsWith("* "))) {
20011
+ addSectionValue(parsed, currentKey, line);
20012
+ }
20013
+ }
20014
+ return {
20015
+ ...parsed,
20016
+ constraints: uniqueStrings8(parsed.constraints).slice(0, 8),
20017
+ antiGoals: uniqueStrings8(parsed.antiGoals).slice(0, 8),
20018
+ rejectedAlternatives: uniqueStrings8(parsed.rejectedAlternatives).slice(0, 8),
20019
+ sourceFields: uniqueStrings8(parsed.sourceFields)
20020
+ };
20021
+ }
20022
+ function hasStructuredIntentFields(intent) {
20023
+ return Boolean(
20024
+ intent?.goal || intent?.owner || intent?.freshnessHorizonDays || (intent?.constraints?.length ?? 0) > 0 || (intent?.antiGoals?.length ?? 0) > 0 || (intent?.rejectedAlternatives?.length ?? 0) > 0
19876
20025
  );
19877
- return uniqueStrings8(matches).slice(0, limit);
19878
20026
  }
19879
- function extractAntiGoals(text) {
19880
- return extractTextMatches(text, INTENT_TEXT_EXTRACTORS.antiGoals, 5);
20027
+ function extractionMetadata(input) {
20028
+ const sectionFields = input.structuredSections.sourceFields.map((field) => `section:${field}`);
20029
+ return {
20030
+ mode: input.structuredFields ? "structured_fields" : sectionFields.length > 0 ? "structured_sections" : "legacy_source_fields",
20031
+ sourceFields: uniqueStrings8([
20032
+ input.structuredFields ? "intent" : void 0,
20033
+ ...sectionFields
20034
+ ]).slice(0, 12),
20035
+ fallbackUsed: input.fallbackUsed
20036
+ };
19881
20037
  }
19882
- function extractRejectedAlternatives(text) {
19883
- return extractTextMatches(text, INTENT_TEXT_EXTRACTORS.rejectedAlternatives, 5);
20038
+ function textHasAnyToken(value, tokens) {
20039
+ const normalized = normalizeIdentifier(value);
20040
+ const parts = normalized.split("_").filter(Boolean);
20041
+ return tokens.some((token) => parts.includes(token));
19884
20042
  }
19885
20043
  function documentLooksLikeIntent(doc) {
19886
- const text = normalizeText([doc.path, doc.title, doc.contentPreview, doc.kind].join(" "));
19887
- return /\b(adr|decision|intent|rationale|constraint|architecture|runbook)\b/.test(text);
20044
+ if (hasStructuredIntentFields(doc.intent)) return true;
20045
+ if (parseIntentSections(doc.contentPreview).sourceFields.length > 0) return true;
20046
+ return [doc.path, doc.title, doc.kind].some(
20047
+ (value) => textHasAnyToken(value ?? "", INTENT_DOCUMENT_KIND_TOKENS)
20048
+ );
19888
20049
  }
19889
- function buildDecisionIntentEntry(decision, changedFiles) {
19890
- const status = intentStatusFromDecision(decision);
20050
+ function buildDecisionIntentEntry(decision, changedFiles, policy) {
20051
+ const status = statusFromDecisionStatus(decision);
19891
20052
  const anchors = inferDecisionAnchors(decision, changedFiles);
19892
- const fullText = decisionText(decision);
20053
+ const sectionIntent = parseIntentSections(
20054
+ [decision.scope, decision.rationale, decision.decision].join("\n")
20055
+ );
20056
+ const structuredFields = hasStructuredIntentFields(decision.intent);
20057
+ const fallbackGoal = decision.rationale || decision.decision || decision.title;
20058
+ const goal = decision.intent?.goal || sectionIntent.goal || fallbackGoal;
19893
20059
  const confidenceScore = normalizeConfidenceScore(
19894
20060
  decision.confidenceScore,
19895
20061
  status === "approved" ? 0.86 : 0.66
19896
20062
  );
20063
+ const freshnessHorizonDays = resolveFreshnessHorizonDays(
20064
+ decision.intent?.freshnessHorizonDays ?? decision.freshnessHorizonDays ?? sectionIntent.freshnessHorizonDays,
20065
+ policy
20066
+ );
19897
20067
  const reasonCodes = uniqueStrings8([
19898
20068
  "decision_intent",
19899
20069
  status === "approved" ? "reviewed_source" : "review_needed",
19900
- anchors.length > 0 ? "affected_anchors_present" : "affected_anchors_missing"
20070
+ anchors.length > 0 ? "affected_anchors_present" : "affected_anchors_missing",
20071
+ structuredFields ? "structured_intent_fields" : void 0,
20072
+ sectionIntent.sourceFields.length > 0 ? "structured_intent_sections" : void 0,
20073
+ goal === fallbackGoal ? "legacy_goal_fallback" : void 0
19901
20074
  ]);
19902
20075
  return {
19903
20076
  id: `intent:${decision.id}`,
@@ -19907,16 +20080,30 @@ function buildDecisionIntentEntry(decision, changedFiles) {
19907
20080
  sourceDecisionId: decision.id,
19908
20081
  sourceDocumentPath: null,
19909
20082
  intent: {
19910
- goal: decision.rationale || decision.decision || decision.title,
20083
+ goal,
19911
20084
  constraints: uniqueStrings8([
19912
20085
  ...decision.constraints ?? [],
19913
- ...splitIntentPhrases(decision.scope)
20086
+ ...decision.intent?.constraints ?? [],
20087
+ ...sectionIntent.constraints
19914
20088
  ]).slice(0, 8),
19915
- antiGoals: extractAntiGoals(fullText),
19916
- rejectedAlternatives: extractRejectedAlternatives(fullText)
20089
+ antiGoals: normalizeIntentList([
20090
+ ...decision.antiGoals ?? [],
20091
+ ...decision.intent?.antiGoals ?? [],
20092
+ ...sectionIntent.antiGoals
20093
+ ]),
20094
+ rejectedAlternatives: normalizeIntentList([
20095
+ ...decision.rejectedAlternatives ?? [],
20096
+ ...decision.intent?.rejectedAlternatives ?? [],
20097
+ ...sectionIntent.rejectedAlternatives
20098
+ ])
19917
20099
  },
20100
+ extraction: extractionMetadata({
20101
+ structuredFields,
20102
+ structuredSections: sectionIntent,
20103
+ fallbackUsed: goal === fallbackGoal
20104
+ }),
19918
20105
  affectedAnchors: anchors,
19919
- owner: null,
20106
+ owner: decision.intent?.owner ?? sectionIntent.owner ?? decision.owner ?? null,
19920
20107
  evidence: decision.evidence && decision.evidence.length > 0 ? decision.evidence : [
19921
20108
  {
19922
20109
  kind: "decision",
@@ -19926,7 +20113,7 @@ function buildDecisionIntentEntry(decision, changedFiles) {
19926
20113
  }
19927
20114
  ],
19928
20115
  confidence: intentConfidence(confidenceScore, status, reasonCodes),
19929
- staleness: stalenessFromStatus(status),
20116
+ staleness: stalenessFromStatus(status, freshnessHorizonDays),
19930
20117
  reasonCodes,
19931
20118
  recommendedActions: status === "approved" ? ["Keep this intent linked when changing its affected anchors."] : ["Review this intent candidate before treating it as governing project truth."],
19932
20119
  caveats: [
@@ -19934,10 +20121,22 @@ function buildDecisionIntentEntry(decision, changedFiles) {
19934
20121
  ]
19935
20122
  };
19936
20123
  }
19937
- function buildDocumentIntentEntry(doc) {
19938
- const preview4 = doc.contentPreview || doc.title || doc.path;
19939
- const reasonCodes = ["document_intent_candidate", "review_needed"];
19940
- const confidenceScore = doc.kind && /adr|decision/i.test(doc.kind) ? 0.66 : 0.58;
20124
+ function buildDocumentIntentEntry(doc, policy) {
20125
+ const sectionIntent = parseIntentSections(doc.contentPreview);
20126
+ const structuredFields = hasStructuredIntentFields(doc.intent);
20127
+ const goal = doc.intent?.goal ?? sectionIntent.goal ?? doc.title ?? doc.path;
20128
+ const reasonCodes = uniqueStrings8([
20129
+ "document_intent_candidate",
20130
+ "review_needed",
20131
+ structuredFields ? "structured_intent_fields" : void 0,
20132
+ sectionIntent.sourceFields.length > 0 ? "structured_intent_sections" : void 0,
20133
+ goal === doc.path || goal === doc.title ? "legacy_goal_fallback" : void 0
20134
+ ]);
20135
+ const confidenceScore = textHasAnyToken(doc.kind ?? "", ["adr", "decision"]) ? 0.66 : 0.58;
20136
+ const freshnessHorizonDays = resolveFreshnessHorizonDays(
20137
+ doc.intent?.freshnessHorizonDays ?? doc.freshnessHorizonDays ?? sectionIntent.freshnessHorizonDays,
20138
+ policy
20139
+ );
19941
20140
  return {
19942
20141
  id: `intent:doc:${doc.path}`,
19943
20142
  title: doc.title || doc.path,
@@ -19946,13 +20145,27 @@ function buildDocumentIntentEntry(doc) {
19946
20145
  sourceDecisionId: null,
19947
20146
  sourceDocumentPath: doc.path,
19948
20147
  intent: {
19949
- goal: preview4,
19950
- constraints: splitIntentPhrases(doc.contentPreview).slice(0, 4),
19951
- antiGoals: extractAntiGoals(preview4),
19952
- rejectedAlternatives: extractRejectedAlternatives(preview4)
20148
+ goal,
20149
+ constraints: uniqueStrings8([
20150
+ ...doc.intent?.constraints ?? [],
20151
+ ...sectionIntent.constraints
20152
+ ]).slice(0, 4),
20153
+ antiGoals: normalizeIntentList([
20154
+ ...doc.intent?.antiGoals ?? [],
20155
+ ...sectionIntent.antiGoals
20156
+ ]),
20157
+ rejectedAlternatives: normalizeIntentList([
20158
+ ...doc.intent?.rejectedAlternatives ?? [],
20159
+ ...sectionIntent.rejectedAlternatives
20160
+ ])
19953
20161
  },
20162
+ extraction: extractionMetadata({
20163
+ structuredFields,
20164
+ structuredSections: sectionIntent,
20165
+ fallbackUsed: goal === doc.path || goal === doc.title
20166
+ }),
19954
20167
  affectedAnchors: [doc.path],
19955
- owner: null,
20168
+ owner: doc.intent?.owner ?? sectionIntent.owner ?? doc.owner ?? null,
19956
20169
  evidence: [
19957
20170
  {
19958
20171
  kind: "document",
@@ -19964,7 +20177,8 @@ function buildDocumentIntentEntry(doc) {
19964
20177
  confidence: intentConfidence(confidenceScore, "review_pending", reasonCodes),
19965
20178
  staleness: {
19966
20179
  state: "unknown",
19967
- reason: "Document-derived intent candidates need review before becoming governing truth."
20180
+ reason: "Document-derived intent candidates need review before becoming governing truth.",
20181
+ horizonDays: freshnessHorizonDays
19968
20182
  },
19969
20183
  reasonCodes,
19970
20184
  recommendedActions: ["Promote, reject, or link this document intent candidate to a decision."],
@@ -19975,15 +20189,19 @@ function buildDocumentIntentEntry(doc) {
19975
20189
  }
19976
20190
  function buildProjectIntentLedger(input, changedFilesOverride) {
19977
20191
  const changedFiles = uniqueStrings8(changedFilesOverride ?? input.changedFiles);
20192
+ const policy = {
20193
+ ...DEFAULT_PROJECT_INTENT_LEDGER_POLICY,
20194
+ ...input.intentPolicy ?? {}
20195
+ };
19978
20196
  const decisionEntries = (input.decisions ?? []).map(
19979
- (decision) => buildDecisionIntentEntry(decision, changedFiles)
20197
+ (decision) => buildDecisionIntentEntry(decision, changedFiles, policy)
19980
20198
  );
19981
20199
  const decisionDocumentPaths = new Set(
19982
20200
  decisionEntries.flatMap(
19983
20201
  (entry) => entry.evidence.map((evidence) => evidence.sourceRef).filter(Boolean)
19984
20202
  )
19985
20203
  );
19986
- const documentEntries = (input.documents ?? []).filter(documentLooksLikeIntent).filter((doc) => !decisionDocumentPaths.has(doc.path)).map(buildDocumentIntentEntry);
20204
+ const documentEntries = (input.documents ?? []).filter(documentLooksLikeIntent).filter((doc) => !decisionDocumentPaths.has(doc.path)).map((doc) => buildDocumentIntentEntry(doc, policy));
19987
20205
  const entries = [...decisionEntries, ...documentEntries];
19988
20206
  const linkedIntentIds = new Set(
19989
20207
  entries.filter((entry) => changedFiles.some((file) => intentEntryMatchesFile(entry, file))).map((entry) => entry.id)
@@ -26093,17 +26311,17 @@ function normalizeCollaborationFiles(files) {
26093
26311
  function parseCollaborationResources(values) {
26094
26312
  return normalizeResources(
26095
26313
  (values ?? []).map((value) => {
26096
- const separatorIndex = value.indexOf(":");
26097
- if (separatorIndex <= 0) {
26314
+ const separatorIndex2 = value.indexOf(":");
26315
+ if (separatorIndex2 <= 0) {
26098
26316
  throw new Error(`Resource must use KIND:id format: ${value}`);
26099
26317
  }
26100
- const kind = normalizeResourceKind(value.slice(0, separatorIndex));
26318
+ const kind = normalizeResourceKind(value.slice(0, separatorIndex2));
26101
26319
  if (!kind) {
26102
26320
  throw new Error(
26103
- `Unsupported collaboration resource kind: ${value.slice(0, separatorIndex)}`
26321
+ `Unsupported collaboration resource kind: ${value.slice(0, separatorIndex2)}`
26104
26322
  );
26105
26323
  }
26106
- const id = value.slice(separatorIndex + 1).trim();
26324
+ const id = value.slice(separatorIndex2 + 1).trim();
26107
26325
  if (!id) {
26108
26326
  throw new Error(`Resource id is required for ${kind}`);
26109
26327
  }
@@ -27633,12 +27851,12 @@ function parseKeyValueEntries(entries, label) {
27633
27851
  }
27634
27852
  const result = {};
27635
27853
  for (const entry of entries) {
27636
- const separatorIndex = entry.indexOf("=");
27637
- if (separatorIndex <= 0 || separatorIndex === entry.length - 1) {
27854
+ const separatorIndex2 = entry.indexOf("=");
27855
+ if (separatorIndex2 <= 0 || separatorIndex2 === entry.length - 1) {
27638
27856
  throw new Error(`Invalid ${label} entry '${entry}'. Expected KEY=value.`);
27639
27857
  }
27640
- const key = entry.slice(0, separatorIndex).trim();
27641
- const value = entry.slice(separatorIndex + 1).trim();
27858
+ const key = entry.slice(0, separatorIndex2).trim();
27859
+ const value = entry.slice(separatorIndex2 + 1).trim();
27642
27860
  if (!key || !value) {
27643
27861
  throw new Error(`Invalid ${label} entry '${entry}'. Expected KEY=value.`);
27644
27862
  }
@@ -201,7 +201,12 @@ snipara-companion workflow resume --include-session-context
201
201
  dirty local evidence, architecture drift, stale/review-pending intent, and
202
202
  heuristic-calibration gaps. Keep `--enforce` opt-in for narrow hooks or
203
203
  calibrated CI adapters; the V1 heuristics are advisory and can produce false
204
- positives on broad text/path matches.
204
+ positives on broad text/path matches. Intent Ledger extraction prefers
205
+ structured contract fields or explicit labeled sections such as `Goal:`,
206
+ `Constraints:`, `Anti-goals:`, `Rejected alternatives:`, `Owner:`, and
207
+ `Freshness horizon days:`. Generic prose remains a legacy fallback for goal
208
+ text only; anti-goals and rejected alternatives are not inferred from loose
209
+ words in free text.
205
210
  - `timeline` is the Git-style log for workflow starts, phase starts, phase
206
211
  commits, final commits, and Team Sync handoffs.
207
212
  - `workflow impact-gate` audits committed local workflow phases that are ahead
@@ -948,7 +953,7 @@ CI adapter needs the contradiction-to-reality gate without a full hosted brief:
948
953
  snipara-companion reality-check \
949
954
  --task "refactor auth middleware" \
950
955
  --changed-files src/auth/middleware.ts \
951
- --decision "DEC-001: auth middleware must keep side effects explicit" \
956
+ --decision $'DEC-001: Goal: keep auth middleware side effects explicit\nConstraints:\n- auth middleware stays synchronous\nAnti-goals:\n- implicit token refresh' \
952
957
  --verification "pnpm test auth" \
953
958
  --enforce
954
959
  ```
@@ -958,7 +963,9 @@ The command reads the local Git scope by default, includes dirty files unless
958
963
  hooks or CI collectors. `--enforce` exits non-zero for `review_required` or
959
964
  `blocking` findings, but should stay opt-in until the matched surfaces,
960
965
  verification signals, and project-specific thresholds have been calibrated.
961
- JSON output is available with `--json`.
966
+ Intent can be supplied as structured sections inside `--decision` or
967
+ `--document`, using labels such as `Goal:`, `Constraints:`, `Anti-goals:`,
968
+ `Rejected alternatives:`, and `Owner:`. JSON output is available with `--json`.
962
969
 
963
970
  Use `intelligence ledger-export` when an agent run, review, or replay benchmark
964
971
  needs a portable Coding Intelligence Ledger instead of a raw transcript:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.0.9",
3
+ "version": "3.0.10",
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": {