@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/index.mjs +10728 -218
- package/dist/index.mjs.map +1 -1
- package/dist/lsp.mjs +1 -0
- package/dist/mcp.mjs +29 -5
- package/package.json +1 -1
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
|
|
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: `${
|
|
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(", ")}.
|
|
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 : [];
|