@majeanson/lac 3.2.0 → 3.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/lsp.mjs CHANGED
@@ -8906,6 +8906,7 @@ const FeatureSchema = object({
8906
8906
  superseded_from: array(string().regex(FEATURE_KEY_PATTERN, "each superseded_from entry must be a valid featureKey")).optional(),
8907
8907
  merged_into: string().regex(FEATURE_KEY_PATTERN, "merged_into must be a valid featureKey").optional(),
8908
8908
  merged_from: array(string().regex(FEATURE_KEY_PATTERN, "each merged_from entry must be a valid featureKey")).optional(),
8909
+ userGuide: string().optional(),
8909
8910
  componentFile: string().optional(),
8910
8911
  npmPackages: array(string()).optional(),
8911
8912
  publicInterface: array(PublicInterfaceEntrySchema).optional(),
package/dist/mcp.mjs CHANGED
@@ -3784,6 +3784,7 @@ const FeatureSchema$1 = object$1({
3784
3784
  superseded_from: array$1(string$2().regex(FEATURE_KEY_PATTERN$1, "each superseded_from entry must be a valid featureKey")).optional(),
3785
3785
  merged_into: string$2().regex(FEATURE_KEY_PATTERN$1, "merged_into must be a valid featureKey").optional(),
3786
3786
  merged_from: array$1(string$2().regex(FEATURE_KEY_PATTERN$1, "each merged_from entry must be a valid featureKey")).optional(),
3787
+ userGuide: string$2().optional(),
3787
3788
  componentFile: string$2().optional(),
3788
3789
  npmPackages: array$1(string$2()).optional(),
3789
3790
  publicInterface: array$1(PublicInterfaceEntrySchema$1).optional(),
@@ -8052,6 +8053,7 @@ const FeatureSchema = object({
8052
8053
  superseded_from: array(string().regex(FEATURE_KEY_PATTERN, "each superseded_from entry must be a valid featureKey")).optional(),
8053
8054
  merged_into: string().regex(FEATURE_KEY_PATTERN, "merged_into must be a valid featureKey").optional(),
8054
8055
  merged_from: array(string().regex(FEATURE_KEY_PATTERN, "each merged_from entry must be a valid featureKey")).optional(),
8056
+ userGuide: string().optional(),
8055
8057
  componentFile: string().optional(),
8056
8058
  npmPackages: array(string()).optional(),
8057
8059
  publicInterface: array(PublicInterfaceEntrySchema).optional(),
@@ -8243,6 +8245,10 @@ Return ONLY a valid JSON array — no other text:
8243
8245
  system: `You are a software engineering analyst. Write a plain-language success criteria statement for this feature — "how do we know it's done and working?" Be specific and testable. 1-3 sentences. Return only the text, no JSON wrapper, no heading.`,
8244
8246
  userSuffix: "Write the success criteria for this feature."
8245
8247
  },
8248
+ userGuide: {
8249
+ system: `You are a technical writer writing for end users — not developers. Given a feature.json, write a plain-language user guide for this feature. Explain what it does and how to use it in everyday language. Avoid technical terms, implementation details, and acceptance-criteria framing. Write from the user's perspective: what they will see, what they can do, and why it helps them. 2-5 sentences or a short bullet list. Return only the guide text, no JSON wrapper, no heading.`,
8250
+ userSuffix: "Write a plain-language user guide for this feature."
8251
+ },
8246
8252
  domain: {
8247
8253
  system: `You are a software engineering analyst. Identify the primary technical domain for this feature from its code and problem statement. Return a single lowercase word or short hyphenated phrase (e.g. "auth", "payments", "notifications", "data-pipeline"). Return only the domain value — nothing else.`,
8248
8254
  userSuffix: "Identify the domain for this feature."
@@ -8312,6 +8318,7 @@ const ALL_FILLABLE_FIELDS = [
8312
8318
  "knownLimitations",
8313
8319
  "tags",
8314
8320
  "successCriteria",
8321
+ "userGuide",
8315
8322
  "domain",
8316
8323
  "componentFile",
8317
8324
  "npmPackages",
@@ -8325,7 +8332,7 @@ function getMissingFields(feature) {
8325
8332
  const val = feature[field];
8326
8333
  if (val === void 0 || val === null) return true;
8327
8334
  if (typeof val === "string") return val.trim().length === 0;
8328
- if (Array.isArray(val)) return val.length === 0;
8335
+ if (Array.isArray(val)) return false;
8329
8336
  return false;
8330
8337
  });
8331
8338
  }
@@ -8392,7 +8399,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [
8392
8399
  },
8393
8400
  {
8394
8401
  name: "create_feature",
8395
- description: "Create a new feature.json in the specified directory.",
8402
+ description: "Create a new feature.json in the specified directory. After creating, immediately call read_feature_context on the same path to analyze surrounding code and fill all required fields before calling advance_feature.",
8396
8403
  inputSchema: {
8397
8404
  type: "object",
8398
8405
  properties: {
@@ -8870,6 +8877,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
8870
8877
  if (raw.superseded_by && !featureKeys.has(String(raw.superseded_by))) issues.push(`broken superseded_by ref: "${raw.superseded_by}" not found`);
8871
8878
  if (raw.merged_into && !featureKeys.has(String(raw.merged_into))) issues.push(`broken merged_into ref: "${raw.merged_into}" not found`);
8872
8879
  for (const key of raw.merged_from ?? []) if (!featureKeys.has(key)) issues.push(`broken merged_from ref: "${key}" not found`);
8880
+ if (feature.status === "active" || feature.status === "draft") {
8881
+ const preFreeze = getMissingForTransition(feature, "frozen");
8882
+ if (preFreeze.length > 0) warnings.push(`will block freeze — missing: ${preFreeze.join(", ")}`);
8883
+ }
8873
8884
  if (raw.superseded_by && feature.status !== "deprecated") warnings.push(`superseded_by set but status is "${feature.status}" — consider deprecating`);
8874
8885
  if (raw.merged_into && feature.status !== "deprecated") warnings.push(`merged_into set but status is "${feature.status}" — consider deprecating`);
8875
8886
  const hasRevisions = Array.isArray(raw.revisions) && raw.revisions.length > 0;
@@ -8961,6 +8972,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
8961
8972
  const feature = result.data;
8962
8973
  const contextStr = contextToString(buildContext(featureDir, feature));
8963
8974
  const missingFields = getMissingFields(feature);
8975
+ let componentFileWarning = "";
8976
+ if (feature.componentFile) {
8977
+ const notFound = feature.componentFile.split(",").map((s) => s.trim()).filter(Boolean).filter((p) => {
8978
+ return [path.resolve(featureDir, p), path.resolve(workspaceRoot, p)].every((c) => !fs.existsSync(c));
8979
+ });
8980
+ if (notFound.length > 0) componentFileWarning = `\n## ⚠ componentFile drift\nThese paths do not exist on disk — update componentFile to match actual source files:\n${notFound.map((p) => ` - ${p}`).join("\n")}\n`;
8981
+ }
8964
8982
  const fieldInstructions = missingFields.map((field) => {
8965
8983
  const prompt = FILL_PROMPTS[field];
8966
8984
  const isJson = JSON_FIELDS.has(field);
@@ -8968,9 +8986,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
8968
8986
  }).join("\n\n");
8969
8987
  const staleAnnotation = feature.annotations?.find((ann) => ann.type === "stale-review");
8970
8988
  const staleWarning = staleAnnotation ? `## ⚠ Stale fields (feature was reopened)\n${staleAnnotation.body}\nReview and rewrite these fields against the current code, then call write_feature_fields.\n\n` : "";
8989
+ const instructions = missingFields.length === 0 ? staleWarning || "All fillable fields are already populated. No generation needed." : `${staleWarning}## Missing fields to fill (${missingFields.join(", ")})\n\nGenerate each field described below, then call write_feature_fields with all values at once. Fill ALL missing fields before calling advance_feature.\n\n${fieldInstructions}`;
8971
8990
  return { content: [{
8972
8991
  type: "text",
8973
- text: `${missingFields.length === 0 ? staleWarning || "All fillable fields are already populated. No generation needed." : `${staleWarning}## Missing fields to fill (${missingFields.join(", ")})\n\nGenerate each field described below, then call write_feature_fields with all values at once. After writing, call advance_feature to check if the feature is ready to transition.\n\n${fieldInstructions}`}\n\n## Context\n\n${contextStr}`
8992
+ text: `${componentFileWarning}${instructions}\n\n## Context\n\n${contextStr}`
8974
8993
  }] };
8975
8994
  }
8976
8995
  case "write_feature_fields": {
@@ -9009,6 +9028,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
9009
9028
  ...existing,
9010
9029
  ...fields
9011
9030
  };
9031
+ updated.lastVerifiedDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9012
9032
  const revisionInput = a.revision;
9013
9033
  let revisionWarning = "";
9014
9034
  if (changingCritical.length > 0) if (revisionInput?.author && revisionInput?.reason) {
@@ -9019,6 +9039,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
9019
9039
  fields_changed: changingCritical,
9020
9040
  reason: revisionInput.reason
9021
9041
  }];
9042
+ updated.annotations = (existing.annotations ?? []).filter((ann) => ann.type !== "stale-review");
9022
9043
  } else revisionWarning = `\n\n⚠ Intent-critical fields changed (${changingCritical.join(", ")}) without a revision entry. Pass a "revision" object with author and reason to attribute this change.`;
9023
9044
  fs.writeFileSync(featurePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
9024
9045
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -9034,7 +9055,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
9034
9055
  const writtenKeys = Object.keys(fields);
9035
9056
  const afterResult = validateFeature(JSON.parse(fs.readFileSync(featurePath, "utf-8")));
9036
9057
  const stillMissing = afterResult.success ? getMissingFields(afterResult.data) : [];
9037
- const nextHint = stillMissing.length > 0 ? `${stillMissing.length} field(s) still missing: ${stillMissing.join(", ")}. Continue filling or call advance_feature to check if the current fields are sufficient to transition.` : `All AI fields filled. Call advance_feature to transition status when ready.`;
9058
+ const nextHint = stillMissing.length > 0 ? `${stillMissing.length} field(s) still missing: ${stillMissing.join(", ")}. Fill all remaining fields with write_feature_fields before calling advance_feature.` : `All AI fields filled. Call advance_feature to transition status when ready.`;
9038
9059
  return { content: [{
9039
9060
  type: "text",
9040
9061
  text: `✓ Wrote ${writtenKeys.length} field(s) to ${featurePath}: ${writtenKeys.join(", ")}\n\n${nextHint}${revisionWarning}`
@@ -9093,6 +9114,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
9093
9114
  ...parsed,
9094
9115
  status: to
9095
9116
  };
9117
+ if (to === "frozen") updated.lastVerifiedDate = today;
9096
9118
  updated.statusHistory = [...updated.statusHistory ?? [], {
9097
9119
  from,
9098
9120
  to,
@@ -9891,7 +9913,9 @@ const REQUIRED_FOR_FROZEN = [
9891
9913
  "implementation",
9892
9914
  "successCriteria",
9893
9915
  "knownLimitations",
9894
- "tags"
9916
+ "tags",
9917
+ "userGuide",
9918
+ "componentFile"
9895
9919
  ];
9896
9920
  function getMissingForTransition(feature, to) {
9897
9921
  const required$2 = to === "active" ? REQUIRED_FOR_ACTIVE : to === "frozen" ? REQUIRED_FOR_FROZEN : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majeanson/lac",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "CLI for life-as-code — feature provenance tracking from the terminal",
5
5
  "license": "MIT",
6
6
  "repository": {