kirograph 1.0.0 → 1.2.1
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 +4 -2
- package/dist/architecture/manifest/python.js +2 -2
- package/dist/architecture/manifest/python.js.map +2 -2
- package/dist/bin/commands/help.js +3 -1
- package/dist/bin/commands/help.js.map +2 -2
- package/dist/bin/commands/memory.js +29 -9
- package/dist/bin/commands/memory.js.map +2 -2
- package/dist/bin/commands/reachability.js +7 -2
- package/dist/bin/commands/reachability.js.map +2 -2
- package/dist/bin/commands/vulns.js +16 -1
- package/dist/bin/commands/vulns.js.map +2 -2
- package/dist/bin/commands/wiki.js +59 -2
- package/dist/bin/commands/wiki.js.map +2 -2
- package/dist/bin/installer/steering.js +9 -0
- package/dist/bin/installer/steering.js.map +2 -2
- package/dist/bin/kirograph.js +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +2 -2
- package/dist/db/memory-schema.sql +2 -1
- package/dist/extraction/extractor.js +46 -41
- package/dist/extraction/extractor.js.map +2 -2
- package/dist/extraction/notebook.js +15 -7
- package/dist/extraction/notebook.js.map +2 -2
- package/dist/jev/client.js +99 -0
- package/dist/jev/client.js.map +7 -0
- package/dist/jev/types.js +17 -0
- package/dist/jev/types.js.map +7 -0
- package/dist/mcp/handlers/memory.js +15 -8
- package/dist/mcp/handlers/memory.js.map +2 -2
- package/dist/mcp/handlers/security.js +5 -3
- package/dist/mcp/handlers/security.js.map +2 -2
- package/dist/mcp/handlers/wiki.js +4 -2
- package/dist/mcp/handlers/wiki.js.map +2 -2
- package/dist/mcp/tools.js +3 -2
- package/dist/mcp/tools.js.map +2 -2
- package/dist/memory/database.js +17 -4
- package/dist/memory/database.js.map +2 -2
- package/dist/memory/index.js +31 -2
- package/dist/memory/index.js.map +3 -3
- package/dist/memory/relation-jev.js +59 -0
- package/dist/memory/relation-jev.js.map +7 -0
- package/dist/memory/schema.js +92 -0
- package/dist/memory/schema.js.map +7 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/patterns/loader.js +2 -146
- package/dist/patterns/loader.js.map +3 -3
- package/dist/security/auth-detection-jev.js +47 -0
- package/dist/security/auth-detection-jev.js.map +7 -0
- package/dist/security/manifest/adapter.js +17 -3
- package/dist/security/manifest/adapter.js.map +3 -3
- package/dist/security/manifest/plugins/cargo.js +23 -0
- package/dist/security/manifest/plugins/cargo.js.map +2 -2
- package/dist/security/manifest/plugins/composer.js +12 -0
- package/dist/security/manifest/plugins/composer.js.map +2 -2
- package/dist/security/manifest/plugins/gradle.js +12 -0
- package/dist/security/manifest/plugins/gradle.js.map +2 -2
- package/dist/security/manifest/plugins/hex.js +12 -0
- package/dist/security/manifest/plugins/hex.js.map +2 -2
- package/dist/security/manifest/plugins/maven.js +101 -4
- package/dist/security/manifest/plugins/maven.js.map +2 -2
- package/dist/security/manifest/plugins/npm.js +117 -2
- package/dist/security/manifest/plugins/npm.js.map +2 -2
- package/dist/security/manifest/plugins/nuget.js +13 -0
- package/dist/security/manifest/plugins/nuget.js.map +2 -2
- package/dist/security/manifest/plugins/pubspec.js +12 -0
- package/dist/security/manifest/plugins/pubspec.js.map +2 -2
- package/dist/security/manifest/plugins/pyproject.js +12 -0
- package/dist/security/manifest/plugins/pyproject.js.map +2 -2
- package/dist/security/manifest/plugins/rubygems.js +12 -0
- package/dist/security/manifest/plugins/rubygems.js.map +2 -2
- package/dist/security/pipeline.js +1 -1
- package/dist/security/pipeline.js.map +2 -2
- package/dist/security/reachability.js +54 -5
- package/dist/security/reachability.js.map +3 -3
- package/dist/security/types.js.map +1 -1
- package/dist/security/vuln/client.js +45 -1
- package/dist/security/vuln/client.js.map +2 -2
- package/dist/security/vuln/osv-adapter.js +45 -1
- package/dist/security/vuln/osv-adapter.js.map +2 -2
- package/dist/shared/json-schema.js +136 -0
- package/dist/shared/json-schema.js.map +7 -0
- package/dist/shared/yaml.js +174 -0
- package/dist/shared/yaml.js.map +7 -0
- package/dist/wiki/contradiction-jev.js +49 -0
- package/dist/wiki/contradiction-jev.js.map +7 -0
- package/dist/wiki/index.js +16 -1
- package/dist/wiki/index.js.map +2 -2
- package/dist/wiki/ingest.js +2 -0
- package/dist/wiki/ingest.js.map +2 -2
- package/dist/wiki/links.js +53 -0
- package/dist/wiki/links.js.map +7 -0
- package/dist/wiki/lint.js +17 -7
- package/dist/wiki/lint.js.map +2 -2
- package/dist/wiki/rename.js +103 -0
- package/dist/wiki/rename.js.map +7 -0
- package/dist/wiki/typed-pages.js +67 -0
- package/dist/wiki/typed-pages.js.map +7 -0
- package/dist/wiki/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/memory/schema.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph Memory \u2014 Opt-in write validation\n *\n * Off by default (config `memoryStrictWrites`): MemoryManager.store() has\n * never validated its input \u2014 `kind` is typed as a fixed union in\n * MemObservationInput, but every call site (CLI `--kind`, the\n * `kirograph_mem_store` MCP tool, direct API use) casts through `any` with\n * no runtime check, so an arbitrary string silently lands in the `kind`\n * column and breaks every downstream consumer that pattern-matches against\n * the known set (search filters, watchmen's passive-capture kind map,\n * timeline rendering). When `memoryStrictWrites` is enabled, writes that\n * don't conform are rejected with the violation named instead of silently\n * corrupting the store.\n *\n * `memorySchemaValidation` (separate, also off by default) goes further:\n * it validates `input.fields` \u2014 free-form structured data beyond `content`\n * \u2014 against a JSON Schema registered per kind at\n * `.kirograph/memory-schemas/<kind>.schema.json`. A kind with no schema\n * file is never validated (schemas are opt-in per kind, not required), so\n * enabling this flag with zero schemas present is a no-op.\n */\nimport * as path from 'path';\nimport type { MemObservationInput, ObservationKind } from './types';\nimport { loadJsonSchema, validateAgainstSchema, SchemaLoadError } from '../shared/json-schema';\n\nexport const OBSERVATION_KINDS: readonly ObservationKind[] = [\n 'decision', 'error', 'pattern', 'architecture', 'summary', 'note',\n];\n\nexport class MemorySchemaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'MemorySchemaError';\n }\n}\n\n/**\n * Validate an observation write against KiroGraph's memory model.\n * Throws MemorySchemaError naming the violation; callers only invoke this\n * when `memoryStrictWrites` is on.\n */\nexport function validateObservationInput(input: MemObservationInput): void {\n const kind = input.kind ?? 'note';\n if (!OBSERVATION_KINDS.includes(kind)) {\n throw new MemorySchemaError(\n `Invalid observation kind \"${kind}\" \u2014 must be one of: ${OBSERVATION_KINDS.join(', ')}`,\n );\n }\n\n if (input.tags?.some(t => typeof t !== 'string' || t.trim() === '')) {\n throw new MemorySchemaError('Observation tags must be non-empty strings');\n }\n}\n\n/**\n * Validate `input.fields` against `.kirograph/memory-schemas/<kind>.schema.json`\n * when a schema is registered for the observation's kind. Only invoked when\n * `memorySchemaValidation` is enabled; throws MemorySchemaError naming every\n * violation when the schema exists and fields don't conform.\n */\nexport function validateObservationFields(input: MemObservationInput, memorySchemasDir: string): void {\n const kind = input.kind ?? 'note';\n const schemaPath = path.join(memorySchemasDir, `${kind}.schema.json`);\n\n let schema;\n try {\n schema = loadJsonSchema(schemaPath);\n } catch (err) {\n if (err instanceof SchemaLoadError) {\n throw new MemorySchemaError(err.message);\n }\n throw err;\n }\n if (!schema) return; // no schema registered for this kind \u2014 nothing to validate\n\n const violations = validateAgainstSchema(input.fields ?? {}, schema);\n if (violations.length > 0) {\n throw new MemorySchemaError(\n `Observation fields for kind \"${kind}\" failed schema validation:\\n - ${violations.join('\\n - ')}`,\n );\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBA,WAAsB;AAEtB,yBAAuE;AAEhE,MAAM,oBAAgD;AAAA,EAC3D;AAAA,EAAY;AAAA,EAAS;AAAA,EAAW;AAAA,EAAgB;AAAA,EAAW;AAC7D;AAEO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,yBAAyB,OAAkC;AACzE,QAAM,OAAO,MAAM,QAAQ;AAC3B,MAAI,CAAC,kBAAkB,SAAS,IAAI,GAAG;AACrC,UAAM,IAAI;AAAA,MACR,6BAA6B,IAAI,4BAAuB,kBAAkB,KAAK,IAAI,CAAC;AAAA,IACtF;AAAA,EACF;AAEA,MAAI,MAAM,MAAM,KAAK,OAAK,OAAO,MAAM,YAAY,EAAE,KAAK,MAAM,EAAE,GAAG;AACnE,UAAM,IAAI,kBAAkB,4CAA4C;AAAA,EAC1E;AACF;AAQO,SAAS,0BAA0B,OAA4B,kBAAgC;AACpG,QAAM,OAAO,MAAM,QAAQ;AAC3B,QAAM,aAAa,KAAK,KAAK,kBAAkB,GAAG,IAAI,cAAc;AAEpE,MAAI;AACJ,MAAI;AACF,iBAAS,mCAAe,UAAU;AAAA,EACpC,SAAS,KAAK;AACZ,QAAI,eAAe,oCAAiB;AAClC,YAAM,IAAI,kBAAkB,IAAI,OAAO;AAAA,IACzC;AACA,UAAM;AAAA,EACR;AACA,MAAI,CAAC,OAAQ;AAEb,QAAM,iBAAa,0CAAsB,MAAM,UAAU,CAAC,GAAG,MAAM;AACnE,MAAI,WAAW,SAAS,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,gCAAgC,IAAI;AAAA,MAAoC,WAAW,KAAK,QAAQ,CAAC;AAAA,IACnG;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/memory/types.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/memory/types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph Memory \u2014 Type definitions\n */\n\n// \u2500\u2500 Session \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemSession {\n id: string;\n ide?: string;\n cwd?: string;\n startedAt: number;\n endedAt?: number;\n}\n\n// \u2500\u2500 Observation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ObservationKind = 'decision' | 'error' | 'pattern' | 'architecture' | 'summary' | 'note';\nexport type ObservationSource = 'hook' | 'manual' | 'agent';\n\nexport interface MemObservation {\n id: string;\n sessionId?: string;\n content: string;\n contentRaw?: string;\n contentHash: string;\n kind: ObservationKind;\n source: ObservationSource;\n tags?: string[];\n createdAt: number;\n /** Temporal validity: when this fact became valid (epoch ms). */\n validFrom?: number;\n /** Temporal validity: when this fact expires (epoch ms). Null = no expiry. */\n validUntil?: number;\n /** ID of the observation that supersedes this one. */\n supersededBy?: string;\n /** Fact type for temporal classification. */\n factType?: 'observation' | 'decision' | 'procedure' | 'constraint';\n topicKey?: string;\n reviewAfter?: number;\n}\n\nexport interface MemObservationInput {\n content: string;\n kind?: ObservationKind;\n source?: ObservationSource;\n tags?: string[];\n topicKey?: string;\n reviewAfter?: number;\n}\n\n// \u2500\u2500 Links \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemLink {\n observationId: string;\n qualifiedName: string;\n relevance: number;\n}\n\n// \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ScoredObservation {\n observation: MemObservation;\n score: number;\n /** Source of the score: 'fts', 'vector', or 'hybrid' */\n scoreSource: 'fts' | 'vector' | 'hybrid';\n /** Active relations for this observation (populated by search) */\n relations?: MemRelation[];\n}\n\nexport interface MemSearchOptions {\n limit?: number;\n kind?: ObservationKind;\n sessionId?: string;\n /** FTS/vector blend: 0 = FTS only, 1 = vector only, 0.5 = equal blend */\n alpha?: number;\n /** Query facts valid at this timestamp (epoch ms). Filters out expired/superseded observations. */\n asOf?: number;\n}\n\nexport interface MemTimelineOptions {\n limit?: number;\n sessionId?: string;\n}\n\n// \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemStats {\n sessions: number;\n activeSessions: number;\n observations: number;\n links: number;\n vectors: number;\n embeddableCount: number;\n modelMismatch: boolean;\n currentModel?: string;\n relations: number;\n pendingConflicts: number;\n}\n\n// \u2500\u2500 Watchmen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface WatchmenReadyResult {\n id: string;\n watchmenReady: true;\n pendingCount: number;\n /** Synthesis instructions for the active agent. */\n message: string;\n /** Project-relative paths to write the workspace brief to. */\n targetFiles: string[];\n /**\n * Present when Kiro is detected (.kiro/ exists). Path to the steering\n * directory where individual skill files should be written as separate\n * `inclusion: manual` files (e.g. `.kiro/steering/watchmen-<slug>.md`).\n * Absent for all other tools \u2014 embed procedures in the brief instead.\n */\n skillTargetDir?: string;\n}\n\n// \u2500\u2500 Compress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CompressResult {\n compressed: string;\n originalLength: number;\n compressedLength: number;\n detectedSymbols: string[];\n}\n\n// \u2500\u2500 Relations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type RelationType =\n | 'supersedes'\n | 'conflicts_with'\n | 'compatible'\n | 'scoped'\n | 'related'\n | 'not_conflict';\n\nexport type JudgmentStatus = 'pending' | 'judged' | 'ignored';\n\nexport interface MemRelation {\n id: string;\n observationA: string;\n observationB: string;\n relation: RelationType;\n confidence: number;\n reason?: string;\n evidence?: string;\n judgmentStatus: JudgmentStatus;\n judgedAt?: number;\n createdAt: number;\n}\n\nexport interface MemRelationInput {\n observationA: string;\n observationB: string;\n relation: RelationType;\n confidence?: number;\n reason?: string;\n evidence?: string;\n}\n\n// \u2500\u2500 Prompts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemPrompt {\n id: string;\n sessionId?: string;\n content: string;\n createdAt: number;\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph Memory \u2014 Type definitions\n */\n\n// \u2500\u2500 Session \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemSession {\n id: string;\n ide?: string;\n cwd?: string;\n startedAt: number;\n endedAt?: number;\n}\n\n// \u2500\u2500 Observation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ObservationKind = 'decision' | 'error' | 'pattern' | 'architecture' | 'summary' | 'note';\nexport type ObservationSource = 'hook' | 'manual' | 'agent';\n\nexport interface MemObservation {\n id: string;\n sessionId?: string;\n content: string;\n contentRaw?: string;\n contentHash: string;\n kind: ObservationKind;\n source: ObservationSource;\n tags?: string[];\n createdAt: number;\n /** Temporal validity: when this fact became valid (epoch ms). */\n validFrom?: number;\n /** Temporal validity: when this fact expires (epoch ms). Null = no expiry. */\n validUntil?: number;\n /** ID of the observation that supersedes this one. */\n supersededBy?: string;\n /** Fact type for temporal classification. */\n factType?: 'observation' | 'decision' | 'procedure' | 'constraint';\n topicKey?: string;\n reviewAfter?: number;\n /**\n * Structured extra fields beyond `content`, validated per-`kind` against\n * `.kirograph/memory-schemas/<kind>.schema.json` when memorySchemaValidation\n * is enabled. Always freely settable \u2014 validation is opt-in.\n */\n fields?: Record<string, unknown>;\n}\n\nexport interface MemObservationInput {\n content: string;\n kind?: ObservationKind;\n source?: ObservationSource;\n tags?: string[];\n topicKey?: string;\n reviewAfter?: number;\n fields?: Record<string, unknown>;\n}\n\n// \u2500\u2500 Links \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemLink {\n observationId: string;\n qualifiedName: string;\n relevance: number;\n}\n\n// \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ScoredObservation {\n observation: MemObservation;\n score: number;\n /** Source of the score: 'fts', 'vector', or 'hybrid' */\n scoreSource: 'fts' | 'vector' | 'hybrid';\n /** Active relations for this observation (populated by search) */\n relations?: MemRelation[];\n}\n\nexport interface MemSearchOptions {\n limit?: number;\n kind?: ObservationKind;\n sessionId?: string;\n /** FTS/vector blend: 0 = FTS only, 1 = vector only, 0.5 = equal blend */\n alpha?: number;\n /** Query facts valid at this timestamp (epoch ms). Filters out expired/superseded observations. */\n asOf?: number;\n}\n\nexport interface MemTimelineOptions {\n limit?: number;\n sessionId?: string;\n}\n\n// \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemStats {\n sessions: number;\n activeSessions: number;\n observations: number;\n links: number;\n vectors: number;\n embeddableCount: number;\n modelMismatch: boolean;\n currentModel?: string;\n relations: number;\n pendingConflicts: number;\n}\n\n// \u2500\u2500 Watchmen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface WatchmenReadyResult {\n id: string;\n watchmenReady: true;\n pendingCount: number;\n /** Synthesis instructions for the active agent. */\n message: string;\n /** Project-relative paths to write the workspace brief to. */\n targetFiles: string[];\n /**\n * Present when Kiro is detected (.kiro/ exists). Path to the steering\n * directory where individual skill files should be written as separate\n * `inclusion: manual` files (e.g. `.kiro/steering/watchmen-<slug>.md`).\n * Absent for all other tools \u2014 embed procedures in the brief instead.\n */\n skillTargetDir?: string;\n}\n\n// \u2500\u2500 Compress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CompressResult {\n compressed: string;\n originalLength: number;\n compressedLength: number;\n detectedSymbols: string[];\n}\n\n// \u2500\u2500 Relations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type RelationType =\n | 'supersedes'\n | 'conflicts_with'\n | 'compatible'\n | 'scoped'\n | 'related'\n | 'not_conflict';\n\nexport type JudgmentStatus = 'pending' | 'judged' | 'ignored';\n\nexport interface MemRelation {\n id: string;\n observationA: string;\n observationB: string;\n relation: RelationType;\n confidence: number;\n reason?: string;\n evidence?: string;\n judgmentStatus: JudgmentStatus;\n judgedAt?: number;\n createdAt: number;\n}\n\nexport interface MemRelationInput {\n observationA: string;\n observationB: string;\n relation: RelationType;\n confidence?: number;\n reason?: string;\n evidence?: string;\n}\n\n// \u2500\u2500 Prompts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MemPrompt {\n id: string;\n sessionId?: string;\n content: string;\n createdAt: number;\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/patterns/loader.js
CHANGED
|
@@ -34,151 +34,7 @@ module.exports = __toCommonJS(loader_exports);
|
|
|
34
34
|
var fs = __toESM(require("fs"));
|
|
35
35
|
var path = __toESM(require("path"));
|
|
36
36
|
var import_errors = require("../errors");
|
|
37
|
-
|
|
38
|
-
const lines = text.split("\n");
|
|
39
|
-
return _parseBlock(lines, 0, 0).result;
|
|
40
|
-
}
|
|
41
|
-
function _parseBlock(lines, startIndex, baseIndent) {
|
|
42
|
-
const result = {};
|
|
43
|
-
let i = startIndex;
|
|
44
|
-
while (i < lines.length) {
|
|
45
|
-
const rawLine = lines[i];
|
|
46
|
-
const commentIdx = rawLine.indexOf(" #");
|
|
47
|
-
const line = commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine;
|
|
48
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
49
|
-
i++;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const indent = _countIndent(rawLine);
|
|
53
|
-
if (indent < baseIndent) break;
|
|
54
|
-
if (indent > baseIndent && Object.keys(result).length === 0) {
|
|
55
|
-
i++;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (indent > baseIndent) {
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
const trimmed = line.trim();
|
|
62
|
-
const colonIdx = trimmed.indexOf(":");
|
|
63
|
-
if (colonIdx <= 0) {
|
|
64
|
-
i++;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const key = trimmed.slice(0, colonIdx).trim();
|
|
68
|
-
const rest = trimmed.slice(colonIdx + 1).trimStart();
|
|
69
|
-
if (rest === "" || rest === "|" || rest === ">") {
|
|
70
|
-
const nextContentLine = _findNextContent(lines, i + 1);
|
|
71
|
-
if (nextContentLine === -1) {
|
|
72
|
-
result[key] = null;
|
|
73
|
-
i++;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const nextIndent = _countIndent(lines[nextContentLine]);
|
|
77
|
-
if (nextIndent > indent) {
|
|
78
|
-
if (lines[nextContentLine].trim().startsWith("- ")) {
|
|
79
|
-
const { items, nextIndex } = _parseSequence(lines, nextContentLine, nextIndent);
|
|
80
|
-
result[key] = items;
|
|
81
|
-
i = nextIndex;
|
|
82
|
-
} else {
|
|
83
|
-
const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, nextIndent);
|
|
84
|
-
result[key] = subResult;
|
|
85
|
-
i = nextIndex;
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
result[key] = null;
|
|
89
|
-
i++;
|
|
90
|
-
}
|
|
91
|
-
} else if (rest.startsWith("[")) {
|
|
92
|
-
result[key] = _parseInlineArray(rest);
|
|
93
|
-
i++;
|
|
94
|
-
} else {
|
|
95
|
-
result[key] = _parseScalar(rest);
|
|
96
|
-
i++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return { result, nextIndex: i };
|
|
100
|
-
}
|
|
101
|
-
function _parseSequence(lines, startIndex, baseIndent) {
|
|
102
|
-
const items = [];
|
|
103
|
-
let i = startIndex;
|
|
104
|
-
while (i < lines.length) {
|
|
105
|
-
const rawLine = lines[i];
|
|
106
|
-
if (rawLine.trim() === "" || rawLine.trim().startsWith("#")) {
|
|
107
|
-
i++;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const indent = _countIndent(rawLine);
|
|
111
|
-
if (indent < baseIndent) break;
|
|
112
|
-
const trimmed = rawLine.trim();
|
|
113
|
-
if (!trimmed.startsWith("- ") && trimmed !== "-") {
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
const itemValue = trimmed.startsWith("- ") ? trimmed.slice(2).trim() : "";
|
|
117
|
-
if (itemValue === "" || itemValue === "|" || itemValue === ">") {
|
|
118
|
-
const nextContentLine = _findNextContent(lines, i + 1);
|
|
119
|
-
if (nextContentLine !== -1 && _countIndent(lines[nextContentLine]) > indent) {
|
|
120
|
-
const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, _countIndent(lines[nextContentLine]));
|
|
121
|
-
items.push(subResult);
|
|
122
|
-
i = nextIndex;
|
|
123
|
-
} else {
|
|
124
|
-
items.push(null);
|
|
125
|
-
i++;
|
|
126
|
-
}
|
|
127
|
-
} else if (itemValue.includes(":")) {
|
|
128
|
-
const itemIndent = indent + 2;
|
|
129
|
-
const continuationLines = [];
|
|
130
|
-
let j = i + 1;
|
|
131
|
-
while (j < lines.length) {
|
|
132
|
-
const nextRaw = lines[j];
|
|
133
|
-
if (nextRaw.trim() === "" || nextRaw.trim().startsWith("#")) {
|
|
134
|
-
j++;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (_countIndent(nextRaw) >= itemIndent) {
|
|
138
|
-
continuationLines.push(nextRaw);
|
|
139
|
-
j++;
|
|
140
|
-
} else {
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const blockLines = [" ".repeat(itemIndent) + itemValue, ...continuationLines];
|
|
145
|
-
const { result: subResult } = _parseBlock(blockLines, 0, itemIndent);
|
|
146
|
-
items.push(subResult);
|
|
147
|
-
i = j;
|
|
148
|
-
} else {
|
|
149
|
-
items.push(_parseScalar(itemValue));
|
|
150
|
-
i++;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { items, nextIndex: i };
|
|
154
|
-
}
|
|
155
|
-
function _parseInlineArray(text) {
|
|
156
|
-
const inner = text.replace(/^\[/, "").replace(/\].*$/, "");
|
|
157
|
-
return inner.split(",").map((s) => _parseScalar(s.trim())).filter((s) => s !== null && s !== void 0);
|
|
158
|
-
}
|
|
159
|
-
function _parseScalar(text) {
|
|
160
|
-
if (text === "true") return true;
|
|
161
|
-
if (text === "false") return false;
|
|
162
|
-
if (text === "null" || text === "~") return null;
|
|
163
|
-
if (/^-?\d+$/.test(text)) return parseInt(text, 10);
|
|
164
|
-
if (/^-?\d+\.\d+$/.test(text)) return parseFloat(text);
|
|
165
|
-
if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
|
|
166
|
-
return text.slice(1, -1);
|
|
167
|
-
}
|
|
168
|
-
return text;
|
|
169
|
-
}
|
|
170
|
-
function _countIndent(line) {
|
|
171
|
-
let i = 0;
|
|
172
|
-
while (i < line.length && line[i] === " ") i++;
|
|
173
|
-
return i;
|
|
174
|
-
}
|
|
175
|
-
function _findNextContent(lines, startIndex) {
|
|
176
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
177
|
-
const t = lines[i].trim();
|
|
178
|
-
if (t !== "" && !t.startsWith("#")) return i;
|
|
179
|
-
}
|
|
180
|
-
return -1;
|
|
181
|
-
}
|
|
37
|
+
var import_yaml = require("../shared/yaml");
|
|
182
38
|
class PatternLibraryLoader {
|
|
183
39
|
/**
|
|
184
40
|
* Load, validate, and merge rules from the builtin path and optional custom path.
|
|
@@ -217,7 +73,7 @@ class PatternLibraryLoader {
|
|
|
217
73
|
const filePath = path.join(dirPath, file);
|
|
218
74
|
try {
|
|
219
75
|
const text = fs.readFileSync(filePath, "utf8");
|
|
220
|
-
const parsed =
|
|
76
|
+
const parsed = (0, import_yaml.parseYaml)(text);
|
|
221
77
|
const rule = this._validate(parsed, filePath);
|
|
222
78
|
if (!rule) continue;
|
|
223
79
|
if (seenIds.has(rule.id)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/patterns/loader.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph PatternLibraryLoader \u2014 loads and merges YAML pattern rule files.\n *\n * js-yaml is not a declared dependency, so this uses a lightweight inline parser\n * that handles the flat key:value + nested rule: block format used by KiroGraph\n * pattern rule files.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { PatternRule } from './types';\nimport { logWarn } from '../errors';\n\n// \u2500\u2500 Minimal YAML parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Supports the subset of YAML used by KiroGraph pattern files:\n// - Flat key: value (string, boolean, number)\n// - key: [item1, item2] (inline array)\n// - key:\\n - item (block sequence for simple values)\n// - rule:\\n (nested block as raw sub-object \u2014 parsed recursively)\n// - Quoted strings: \"...\" and '...'\n// - Comments: # ...\n\nfunction _parseYaml(text: string): Record<string, unknown> {\n const lines = text.split('\\n');\n return _parseBlock(lines, 0, 0).result;\n}\n\ninterface BlockResult {\n result: Record<string, unknown>;\n nextIndex: number;\n}\n\nfunction _parseBlock(lines: string[], startIndex: number, baseIndent: number): BlockResult {\n const result: Record<string, unknown> = {};\n let i = startIndex;\n\n while (i < lines.length) {\n const rawLine = lines[i];\n // Strip trailing comments (but not inside strings \u2014 simplified approach)\n const commentIdx = rawLine.indexOf(' #');\n const line = commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine;\n\n // Skip blank lines\n if (line.trim() === '' || line.trim().startsWith('#')) {\n i++;\n continue;\n }\n\n const indent = _countIndent(rawLine);\n\n // If we've de-indented past our base, stop\n if (indent < baseIndent) break;\n\n // Skip lines that are at an indent level below our block's base\n if (indent > baseIndent && Object.keys(result).length === 0) {\n // Haven't started the block yet \u2014 shouldn't happen but skip\n i++;\n continue;\n }\n\n if (indent > baseIndent) {\n // This belongs to a child block already being processed \u2014 stop\n break;\n }\n\n const trimmed = line.trim();\n\n // Detect key: ... pattern\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) {\n i++;\n continue;\n }\n\n const key = trimmed.slice(0, colonIdx).trim();\n const rest = trimmed.slice(colonIdx + 1).trimStart();\n\n if (rest === '' || rest === '|' || rest === '>') {\n // Value is on the following lines \u2014 could be a nested block or block scalar\n // Peek ahead: if next non-empty line is indented more, recurse\n const nextContentLine = _findNextContent(lines, i + 1);\n if (nextContentLine === -1) {\n result[key] = null;\n i++;\n continue;\n }\n const nextIndent = _countIndent(lines[nextContentLine]);\n if (nextIndent > indent) {\n // Check if next line is a sequence item (- ...)\n if (lines[nextContentLine].trim().startsWith('- ')) {\n const { items, nextIndex } = _parseSequence(lines, nextContentLine, nextIndent);\n result[key] = items;\n i = nextIndex;\n } else {\n const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, nextIndent);\n result[key] = subResult;\n i = nextIndex;\n }\n } else {\n result[key] = null;\n i++;\n }\n } else if (rest.startsWith('[')) {\n // Inline array: [a, b, c]\n result[key] = _parseInlineArray(rest);\n i++;\n } else {\n // Scalar value\n result[key] = _parseScalar(rest);\n i++;\n }\n }\n\n return { result, nextIndex: i };\n}\n\nfunction _parseSequence(lines: string[], startIndex: number, baseIndent: number): { items: unknown[]; nextIndex: number } {\n const items: unknown[] = [];\n let i = startIndex;\n\n while (i < lines.length) {\n const rawLine = lines[i];\n if (rawLine.trim() === '' || rawLine.trim().startsWith('#')) {\n i++;\n continue;\n }\n const indent = _countIndent(rawLine);\n if (indent < baseIndent) break;\n\n const trimmed = rawLine.trim();\n if (!trimmed.startsWith('- ') && trimmed !== '-') {\n break;\n }\n\n const itemValue = trimmed.startsWith('- ') ? trimmed.slice(2).trim() : '';\n\n if (itemValue === '' || itemValue === '|' || itemValue === '>') {\n // Multi-line sequence item\n const nextContentLine = _findNextContent(lines, i + 1);\n if (nextContentLine !== -1 && _countIndent(lines[nextContentLine]) > indent) {\n const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, _countIndent(lines[nextContentLine]));\n items.push(subResult);\n i = nextIndex;\n } else {\n items.push(null);\n i++;\n }\n } else if (itemValue.includes(':')) {\n // Inline key:value item \u2014 parse as sub-object, collecting any indented continuation lines\n // Find continuation lines indented deeper than this item\n const itemIndent = indent + 2;\n const continuationLines: string[] = [];\n let j = i + 1;\n while (j < lines.length) {\n const nextRaw = lines[j];\n if (nextRaw.trim() === '' || nextRaw.trim().startsWith('#')) { j++; continue; }\n if (_countIndent(nextRaw) >= itemIndent) {\n continuationLines.push(nextRaw);\n j++;\n } else {\n break;\n }\n }\n const blockLines = [' '.repeat(itemIndent) + itemValue, ...continuationLines];\n const { result: subResult } = _parseBlock(blockLines, 0, itemIndent);\n items.push(subResult);\n i = j;\n } else {\n items.push(_parseScalar(itemValue));\n i++;\n }\n }\n\n return { items, nextIndex: i };\n}\n\nfunction _parseInlineArray(text: string): string[] {\n const inner = text.replace(/^\\[/, '').replace(/\\].*$/, '');\n return inner.split(',').map(s => _parseScalar(s.trim()) as string).filter(s => s !== null && s !== undefined) as string[];\n}\n\nfunction _parseScalar(text: string): unknown {\n if (text === 'true') return true;\n if (text === 'false') return false;\n if (text === 'null' || text === '~') return null;\n if (/^-?\\d+$/.test(text)) return parseInt(text, 10);\n if (/^-?\\d+\\.\\d+$/.test(text)) return parseFloat(text);\n // Quoted string\n if ((text.startsWith('\"') && text.endsWith('\"')) || (text.startsWith(\"'\") && text.endsWith(\"'\"))) {\n return text.slice(1, -1);\n }\n return text;\n}\n\nfunction _countIndent(line: string): number {\n let i = 0;\n while (i < line.length && line[i] === ' ') i++;\n return i;\n}\n\nfunction _findNextContent(lines: string[], startIndex: number): number {\n for (let i = startIndex; i < lines.length; i++) {\n const t = lines[i].trim();\n if (t !== '' && !t.startsWith('#')) return i;\n }\n return -1;\n}\n\n// \u2500\u2500 PatternLibraryLoader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class PatternLibraryLoader {\n /**\n * Load, validate, and merge rules from the builtin path and optional custom path.\n * User-supplied rules override bundled rules on id collision.\n */\n load(builtinPath: string, customPath?: string): PatternRule[] {\n const builtin = this._loadDirectory(builtinPath, 'builtin');\n const builtinMap = new Map<string, PatternRule>();\n for (const rule of builtin) builtinMap.set(rule.id, rule);\n\n if (!customPath) return builtin;\n\n const custom = this._loadDirectory(customPath, 'custom');\n const merged = new Map<string, PatternRule>(builtinMap);\n for (const rule of custom) {\n if (merged.has(rule.id)) {\n logWarn(`PatternLibraryLoader: custom rule \"${rule.id}\" overrides bundled rule`);\n }\n merged.set(rule.id, rule);\n }\n\n return [...merged.values()];\n }\n\n private _loadDirectory(dirPath: string, source: string): PatternRule[] {\n if (!fs.existsSync(dirPath)) {\n logWarn(`PatternLibraryLoader: ${source} library path does not exist: ${dirPath}`);\n return [];\n }\n\n const rules: PatternRule[] = [];\n const seenIds = new Map<string, string>();\n\n let files: string[];\n try {\n files = fs.readdirSync(dirPath).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to read ${source} directory \"${dirPath}\": ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n for (const file of files) {\n const filePath = path.join(dirPath, file);\n try {\n const text = fs.readFileSync(filePath, 'utf8');\n const parsed = _parseYaml(text);\n const rule = this._validate(parsed, filePath);\n if (!rule) continue;\n\n if (seenIds.has(rule.id)) {\n logWarn(`PatternLibraryLoader: duplicate rule id \"${rule.id}\" in ${source} library \u2014 found in \"${seenIds.get(rule.id)}\" and \"${filePath}\", using last loaded`);\n }\n seenIds.set(rule.id, filePath);\n rules.push(rule);\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to load rule file \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return rules;\n }\n\n private _validate(parsed: Record<string, unknown>, filePath: string): PatternRule | null {\n const required = ['id', 'language', 'severity', 'owaspCategory', 'description', 'fixHint', 'rule'];\n for (const field of required) {\n if (parsed[field] === undefined || parsed[field] === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" is missing required field \"${field}\", skipping`);\n return null;\n }\n }\n\n const id = String(parsed['id']);\n const language = parsed['language'];\n const severity = String(parsed['severity']) as PatternRule['severity'];\n const owaspCategory = String(parsed['owaspCategory']);\n const description = String(parsed['description']);\n const fixHint = String(parsed['fixHint']);\n const rule = parsed['rule'];\n\n const validSeverities = new Set(['critical', 'high', 'medium', 'low']);\n if (!validSeverities.has(severity)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid severity \"${severity}\", skipping`);\n return null;\n }\n\n if (typeof language !== 'string' && !Array.isArray(language)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid language field, skipping`);\n return null;\n }\n\n if (typeof rule !== 'object' || Array.isArray(rule) || rule === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid rule field (must be an object), skipping`);\n return null;\n }\n\n return {\n id,\n language: Array.isArray(language) ? (language as string[]) : String(language),\n severity,\n owaspCategory,\n description,\n fixHint,\n rule: rule as Record<string, unknown>,\n };\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,SAAoB;AACpB,WAAsB;AAEtB,oBAAwB;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph PatternLibraryLoader \u2014 loads and merges YAML pattern rule files.\n *\n * js-yaml is not a declared dependency, so this uses the lightweight shared\n * YAML parser (src/shared/yaml.ts) that handles the flat key:value + nested\n * rule: block format used by KiroGraph pattern rule files.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { PatternRule } from './types';\nimport { logWarn } from '../errors';\nimport { parseYaml as _parseYaml } from '../shared/yaml';\n\n// \u2500\u2500 PatternLibraryLoader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class PatternLibraryLoader {\n /**\n * Load, validate, and merge rules from the builtin path and optional custom path.\n * User-supplied rules override bundled rules on id collision.\n */\n load(builtinPath: string, customPath?: string): PatternRule[] {\n const builtin = this._loadDirectory(builtinPath, 'builtin');\n const builtinMap = new Map<string, PatternRule>();\n for (const rule of builtin) builtinMap.set(rule.id, rule);\n\n if (!customPath) return builtin;\n\n const custom = this._loadDirectory(customPath, 'custom');\n const merged = new Map<string, PatternRule>(builtinMap);\n for (const rule of custom) {\n if (merged.has(rule.id)) {\n logWarn(`PatternLibraryLoader: custom rule \"${rule.id}\" overrides bundled rule`);\n }\n merged.set(rule.id, rule);\n }\n\n return [...merged.values()];\n }\n\n private _loadDirectory(dirPath: string, source: string): PatternRule[] {\n if (!fs.existsSync(dirPath)) {\n logWarn(`PatternLibraryLoader: ${source} library path does not exist: ${dirPath}`);\n return [];\n }\n\n const rules: PatternRule[] = [];\n const seenIds = new Map<string, string>();\n\n let files: string[];\n try {\n files = fs.readdirSync(dirPath).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to read ${source} directory \"${dirPath}\": ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n for (const file of files) {\n const filePath = path.join(dirPath, file);\n try {\n const text = fs.readFileSync(filePath, 'utf8');\n const parsed = _parseYaml(text);\n const rule = this._validate(parsed, filePath);\n if (!rule) continue;\n\n if (seenIds.has(rule.id)) {\n logWarn(`PatternLibraryLoader: duplicate rule id \"${rule.id}\" in ${source} library \u2014 found in \"${seenIds.get(rule.id)}\" and \"${filePath}\", using last loaded`);\n }\n seenIds.set(rule.id, filePath);\n rules.push(rule);\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to load rule file \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return rules;\n }\n\n private _validate(parsed: Record<string, unknown>, filePath: string): PatternRule | null {\n const required = ['id', 'language', 'severity', 'owaspCategory', 'description', 'fixHint', 'rule'];\n for (const field of required) {\n if (parsed[field] === undefined || parsed[field] === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" is missing required field \"${field}\", skipping`);\n return null;\n }\n }\n\n const id = String(parsed['id']);\n const language = parsed['language'];\n const severity = String(parsed['severity']) as PatternRule['severity'];\n const owaspCategory = String(parsed['owaspCategory']);\n const description = String(parsed['description']);\n const fixHint = String(parsed['fixHint']);\n const rule = parsed['rule'];\n\n const validSeverities = new Set(['critical', 'high', 'medium', 'low']);\n if (!validSeverities.has(severity)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid severity \"${severity}\", skipping`);\n return null;\n }\n\n if (typeof language !== 'string' && !Array.isArray(language)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid language field, skipping`);\n return null;\n }\n\n if (typeof rule !== 'object' || Array.isArray(rule) || rule === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid rule field (must be an object), skipping`);\n return null;\n }\n\n return {\n id,\n language: Array.isArray(language) ? (language as string[]) : String(language),\n severity,\n owaspCategory,\n description,\n fixHint,\n rule: rule as Record<string, unknown>,\n };\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,SAAoB;AACpB,WAAsB;AAEtB,oBAAwB;AACxB,kBAAwC;AAIjC,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhC,KAAK,aAAqB,YAAoC;AAC5D,UAAM,UAAU,KAAK,eAAe,aAAa,SAAS;AAC1D,UAAM,aAAa,oBAAI,IAAyB;AAChD,eAAW,QAAQ,QAAS,YAAW,IAAI,KAAK,IAAI,IAAI;AAExD,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,SAAS,KAAK,eAAe,YAAY,QAAQ;AACvD,UAAM,SAAS,IAAI,IAAyB,UAAU;AACtD,eAAW,QAAQ,QAAQ;AACzB,UAAI,OAAO,IAAI,KAAK,EAAE,GAAG;AACvB,mCAAQ,sCAAsC,KAAK,EAAE,0BAA0B;AAAA,MACjF;AACA,aAAO,IAAI,KAAK,IAAI,IAAI;AAAA,IAC1B;AAEA,WAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAAA,EAC5B;AAAA,EAEQ,eAAe,SAAiB,QAA+B;AACrE,QAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AAC3B,iCAAQ,yBAAyB,MAAM,iCAAiC,OAAO,EAAE;AACjF,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,QAAuB,CAAC;AAC9B,UAAM,UAAU,oBAAI,IAAoB;AAExC,QAAI;AACJ,QAAI;AACF,cAAQ,GAAG,YAAY,OAAO,EAAE,OAAO,OAAK,EAAE,SAAS,OAAO,KAAK,EAAE,SAAS,MAAM,CAAC;AAAA,IACvF,SAAS,KAAK;AACZ,iCAAQ,wCAAwC,MAAM,eAAe,OAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AACpI,aAAO,CAAC;AAAA,IACV;AAEA,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,KAAK,KAAK,SAAS,IAAI;AACxC,UAAI;AACF,cAAM,OAAO,GAAG,aAAa,UAAU,MAAM;AAC7C,cAAM,aAAS,YAAAA,WAAW,IAAI;AAC9B,cAAM,OAAO,KAAK,UAAU,QAAQ,QAAQ;AAC5C,YAAI,CAAC,KAAM;AAEX,YAAI,QAAQ,IAAI,KAAK,EAAE,GAAG;AACxB,qCAAQ,4CAA4C,KAAK,EAAE,QAAQ,MAAM,6BAAwB,QAAQ,IAAI,KAAK,EAAE,CAAC,UAAU,QAAQ,sBAAsB;AAAA,QAC/J;AACA,gBAAQ,IAAI,KAAK,IAAI,QAAQ;AAC7B,cAAM,KAAK,IAAI;AAAA,MACjB,SAAS,KAAK;AACZ,mCAAQ,mDAAmD,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,MAC7H;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,QAAiC,UAAsC;AACvF,UAAM,WAAW,CAAC,MAAM,YAAY,YAAY,iBAAiB,eAAe,WAAW,MAAM;AACjG,eAAW,SAAS,UAAU;AAC5B,UAAI,OAAO,KAAK,MAAM,UAAa,OAAO,KAAK,MAAM,MAAM;AACzD,mCAAQ,oCAAoC,QAAQ,gCAAgC,KAAK,aAAa;AACtG,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,KAAK,OAAO,OAAO,IAAI,CAAC;AAC9B,UAAM,WAAW,OAAO,UAAU;AAClC,UAAM,WAAW,OAAO,OAAO,UAAU,CAAC;AAC1C,UAAM,gBAAgB,OAAO,OAAO,eAAe,CAAC;AACpD,UAAM,cAAc,OAAO,OAAO,aAAa,CAAC;AAChD,UAAM,UAAU,OAAO,OAAO,SAAS,CAAC;AACxC,UAAM,OAAO,OAAO,MAAM;AAE1B,UAAM,kBAAkB,oBAAI,IAAI,CAAC,YAAY,QAAQ,UAAU,KAAK,CAAC;AACrE,QAAI,CAAC,gBAAgB,IAAI,QAAQ,GAAG;AAClC,iCAAQ,oCAAoC,QAAQ,2BAA2B,QAAQ,aAAa;AACpG,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,GAAG;AAC5D,iCAAQ,oCAAoC,QAAQ,wCAAwC;AAC5F,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,KAAK,SAAS,MAAM;AACpE,iCAAQ,oCAAoC,QAAQ,wDAAwD;AAC5G,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL;AAAA,MACA,UAAU,MAAM,QAAQ,QAAQ,IAAK,WAAwB,OAAO,QAAQ;AAAA,MAC5E;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": ["_parseYaml"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var auth_detection_jev_exports = {};
|
|
20
|
+
__export(auth_detection_jev_exports, {
|
|
21
|
+
checkAuthWithJev: () => checkAuthWithJev
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(auth_detection_jev_exports);
|
|
24
|
+
async function checkAuthWithJev(client, routeName, callPathNames) {
|
|
25
|
+
const state = `Route: ${routeName}
|
|
26
|
+
|
|
27
|
+
Functions/middleware on its call path: ${callPathNames.length > 0 ? callPathNames.join(", ") : "(none found)"}`;
|
|
28
|
+
const response = await client.ask(state, {
|
|
29
|
+
authenticated: {
|
|
30
|
+
type: "noul",
|
|
31
|
+
instructions: "This route is protected by authentication or authorization before its handler logic runs (a session check, token verification, login guard, permission check, etc. somewhere on its call path)."
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const answer = response.answers.authenticated;
|
|
35
|
+
if (!answer || answer.type !== "noul") {
|
|
36
|
+
throw new Error('jev did not return a noul answer for "authenticated"');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
authenticated: answer.noul >= 0.5,
|
|
40
|
+
confidence: answer.confidence ?? answer.noul
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
44
|
+
0 && (module.exports = {
|
|
45
|
+
checkAuthWithJev
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=auth-detection-jev.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/security/auth-detection-jev.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph-Sec \u2014 jev-based auth detection (opt-in, securityAuthDetectionMode: 'jev')\n *\n * Backstop for AttackSurfaceAnalyzer's isAuthenticated heuristic: only called\n * when the substring-match heuristic finds no auth pattern on the call path,\n * to catch custom-named auth wrappers the heuristic would otherwise silently\n * misclassify as unauthenticated. The heuristic's positive matches are\n * trusted as-is and never re-checked, keeping this cheap.\n */\nimport type { JevClient } from '../jev/client';\n\nexport interface JevAuthResult {\n authenticated: boolean;\n confidence: number;\n}\n\nexport async function checkAuthWithJev(\n client: JevClient,\n routeName: string,\n callPathNames: string[],\n): Promise<JevAuthResult> {\n const state = `Route: ${routeName}\\n\\nFunctions/middleware on its call path: ${callPathNames.length > 0 ? callPathNames.join(', ') : '(none found)'}`;\n const response = await client.ask(state, {\n authenticated: {\n type: 'noul',\n instructions: 'This route is protected by authentication or authorization before its handler logic runs (a session check, token verification, login guard, permission check, etc. somewhere on its call path).',\n },\n });\n\n const answer = response.answers.authenticated;\n if (!answer || answer.type !== 'noul') {\n throw new Error('jev did not return a noul answer for \"authenticated\"');\n }\n\n return {\n authenticated: answer.noul >= 0.5,\n confidence: answer.confidence ?? answer.noul,\n };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,eAAsB,iBACpB,QACA,WACA,eACwB;AACxB,QAAM,QAAQ,UAAU,SAAS;AAAA;AAAA,yCAA8C,cAAc,SAAS,IAAI,cAAc,KAAK,IAAI,IAAI,cAAc;AACnJ,QAAM,WAAW,MAAM,OAAO,IAAI,OAAO;AAAA,IACvC,eAAe;AAAA,MACb,MAAM;AAAA,MACN,cAAc;AAAA,IAChB;AAAA,EACF,CAAC;AAED,QAAM,SAAS,SAAS,QAAQ;AAChC,MAAI,CAAC,UAAU,OAAO,SAAS,QAAQ;AACrC,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AAEA,SAAO;AAAA,IACL,eAAe,OAAO,QAAQ;AAAA,IAC9B,YAAY,OAAO,cAAc,OAAO;AAAA,EAC1C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -133,6 +133,7 @@ class SecurityManifestAdapter {
|
|
|
133
133
|
if (!archParser) continue;
|
|
134
134
|
const relManifest = path.relative(this.projectRoot, manifestPath).replace(/\\/g, "/");
|
|
135
135
|
const plugin = this.plugins.get(archParser.name);
|
|
136
|
+
const standalonePlugin = !plugin || !plugin.canExtract(manifestPath) ? [...this.standalonePlugins.values()].find((p) => p.canExtract(manifestPath)) : void 0;
|
|
136
137
|
if (plugin && plugin.canExtract(manifestPath)) {
|
|
137
138
|
try {
|
|
138
139
|
const deps = await plugin.extract(manifestPath, this.projectRoot);
|
|
@@ -142,6 +143,15 @@ class SecurityManifestAdapter {
|
|
|
142
143
|
const message = err instanceof Error ? err.message : String(err);
|
|
143
144
|
errors.push({ file: relManifest, message });
|
|
144
145
|
}
|
|
146
|
+
} else if (standalonePlugin) {
|
|
147
|
+
try {
|
|
148
|
+
const deps = await standalonePlugin.extract(manifestPath, this.projectRoot);
|
|
149
|
+
allDeps.push(...deps);
|
|
150
|
+
manifestsParsed++;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
+
errors.push({ file: relManifest, message });
|
|
154
|
+
}
|
|
145
155
|
} else {
|
|
146
156
|
try {
|
|
147
157
|
const packages = await archParser.parse(manifestPath, this.projectRoot);
|
|
@@ -175,9 +185,9 @@ class SecurityManifestAdapter {
|
|
|
175
185
|
async extractFromManifest(manifestPath) {
|
|
176
186
|
const archParser = this.archParsers.find((p) => p.canParse(manifestPath));
|
|
177
187
|
if (!archParser) {
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
return
|
|
188
|
+
const standalonePlugin2 = [...this.standalonePlugins.values()].find((p) => p.canExtract(manifestPath));
|
|
189
|
+
if (standalonePlugin2) {
|
|
190
|
+
return standalonePlugin2.extract(manifestPath, this.projectRoot);
|
|
181
191
|
}
|
|
182
192
|
return [];
|
|
183
193
|
}
|
|
@@ -185,6 +195,10 @@ class SecurityManifestAdapter {
|
|
|
185
195
|
if (plugin && plugin.canExtract(manifestPath)) {
|
|
186
196
|
return plugin.extract(manifestPath, this.projectRoot);
|
|
187
197
|
}
|
|
198
|
+
const standalonePlugin = [...this.standalonePlugins.values()].find((p) => p.canExtract(manifestPath));
|
|
199
|
+
if (standalonePlugin) {
|
|
200
|
+
return standalonePlugin.extract(manifestPath, this.projectRoot);
|
|
201
|
+
}
|
|
188
202
|
const packages = await archParser.parse(manifestPath, this.projectRoot);
|
|
189
203
|
const relManifest = path.relative(this.projectRoot, manifestPath).replace(/\\/g, "/");
|
|
190
204
|
return this._fallbackExtract(packages, archParser.name, relManifest);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/security/manifest/adapter.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SecurityManifestAdapter\n *\n * Wraps the existing architecture manifest parsers (11 parsers: npm, cargo, go,\n * gradle, maven, csproj, python, scala, elm, ocaml) to additionally extract\n * version constraints and dependency scopes that the architecture module does\n * not capture.\n *\n * The architecture parsers discover manifest files and extract package names into\n * `ArchPackage.externalDeps`. This adapter reuses that discovery infrastructure\n * and layers version/scope extraction on top via ecosystem-specific plugins.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { getAllManifestParsers } from '../../architecture/manifest/index';\nimport type { ManifestParser as ArchManifestParser, ArchPackage } from '../../architecture/types';\nimport type { ParsedDependency, ManifestParseResult } from '../types';\n\n// \u2500\u2500 Include/Exclude filter support \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ManifestScanOptions {\n /** Glob patterns for paths to include (relative to projectRoot). Empty = all. */\n include?: string[];\n /** Glob patterns for paths to exclude (relative to projectRoot). */\n exclude?: string[];\n}\n\n// \u2500\u2500 Plugin Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * A version extraction plugin provides ecosystem-specific logic to extract\n * version constraints and scopes from manifest files. Each plugin corresponds\n * to one or more architecture parsers and enhances their output with security-\n * relevant metadata.\n */\nexport interface VersionExtractionPlugin {\n /** Ecosystem identifier matching the architecture parser name (e.g. 'npm', 'maven', 'go') */\n ecosystem: string;\n /** Manifest filenames this plugin handles (should match the architecture parser's manifestFiles) */\n manifestFiles: string[];\n /** Returns true if this plugin can handle the given manifest file path */\n canExtract(manifestPath: string): boolean;\n /**\n * Extract version constraints and scopes from a manifest file.\n * This is called AFTER the architecture parser has already discovered the manifest.\n * The plugin receives the manifest path and project root, and returns detailed\n * dependency information including versions and scopes.\n */\n extract(manifestPath: string, projectRoot: string): Promise<ParsedDependency[]>;\n}\n\n// \u2500\u2500 Directories to skip (mirrors architecture module's SKIP_DIRS) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SKIP_DIRS = new Set([\n 'node_modules', '.git', 'dist', 'build', 'target', '.gradle', '__pycache__',\n '.kirograph', 'vendor', '.cache', 'coverage', '.nyc_output', '_build', '_opam',\n 'elm-stuff', 'zig-cache', 'zig-out',\n]);\n\n// \u2500\u2500 SecurityManifestAdapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class SecurityManifestAdapter {\n private readonly archParsers: ArchManifestParser[];\n private readonly plugins: Map<string, VersionExtractionPlugin> = new Map();\n /** Standalone plugins have no corresponding architecture parser \u2014 they own their own discovery. */\n private readonly standalonePlugins: Map<string, VersionExtractionPlugin> = new Map();\n private readonly projectRoot: string;\n private readonly scanOptions: ManifestScanOptions;\n\n constructor(projectRoot: string, scanOptions?: ManifestScanOptions) {\n this.projectRoot = projectRoot;\n this.scanOptions = scanOptions ?? {};\n this.archParsers = getAllManifestParsers();\n }\n\n /**\n * Register a version extraction plugin for an ecosystem that has a\n * corresponding architecture parser. The plugin's `ecosystem` field\n * must equal the architecture parser's `name` for the lookup to work.\n */\n registerPlugin(plugin: VersionExtractionPlugin): void {\n this.plugins.set(plugin.ecosystem, plugin);\n }\n\n /**\n * Register a standalone version extraction plugin for an ecosystem that\n * has NO corresponding architecture parser. The plugin's `manifestFiles`\n * list drives discovery \u2014 the adapter will scan for those filenames and\n * invoke the plugin directly when found.\n */\n registerStandalonePlugin(plugin: VersionExtractionPlugin): void {\n this.standalonePlugins.set(plugin.ecosystem, plugin);\n }\n\n /**\n * Get all registered version extraction plugins (wrapped + standalone).\n */\n getPlugins(): VersionExtractionPlugin[] {\n return [...this.plugins.values(), ...this.standalonePlugins.values()];\n }\n\n /**\n * Get the architecture parsers being wrapped.\n */\n getArchParsers(): ArchManifestParser[] {\n return this.archParsers;\n }\n\n /**\n * Discover all manifest files in the project tree.\n * Includes files known to architecture parsers AND files from standalone plugins.\n * Respects config include/exclude patterns when provided.\n * Returns absolute paths.\n */\n discoverManifests(): string[] {\n const allManifests = this._findManifests(this.projectRoot);\n\n // If no include patterns, return all discovered manifests\n if (!this.scanOptions.include || this.scanOptions.include.length === 0) {\n return allManifests;\n }\n\n // Filter manifests to only those within included paths\n const picomatch = require('picomatch');\n const includeMatchers = this.scanOptions.include.map((p: string) => picomatch(p));\n const excludeMatchers = (this.scanOptions.exclude ?? []).map((p: string) => picomatch(p));\n\n return allManifests.filter(absPath => {\n const rel = path.relative(this.projectRoot, absPath).replace(/\\\\/g, '/');\n const included = includeMatchers.some((m: (s: string) => boolean) => m(rel));\n if (!included) return false;\n const excluded = excludeMatchers.some((m: (s: string) => boolean) => m(rel));\n return !excluded;\n });\n }\n\n /**\n * Run full discovery and extraction:\n * 1. Discover manifest files (reusing architecture's SKIP_DIRS logic)\n * 2. For each manifest, find the matching architecture parser\n * 3. If a version extraction plugin is registered for that ecosystem,\n * use it to extract detailed version/scope information\n * 4. Otherwise, fall back to basic extraction from the architecture parser's\n * ArchPackage.externalDeps (names only, no version info)\n */\n async extractAll(): Promise<ManifestParseResult> {\n const manifestPaths = this.discoverManifests();\n const allDeps: ParsedDependency[] = [];\n const warnings: Array<{ file: string; line?: number; message: string }> = [];\n const errors: Array<{ file: string; message: string }> = [];\n let manifestsParsed = 0;\n\n for (const manifestPath of manifestPaths) {\n const archParser = this.archParsers.find(p => p.canParse(manifestPath));\n if (!archParser) continue;\n\n const relManifest = path.relative(this.projectRoot, manifestPath).replace(/\\\\/g, '/');\n const plugin = this.plugins.get(archParser.name);\n\n if (plugin && plugin.canExtract(manifestPath)) {\n // Use the version extraction plugin for detailed parsing\n try {\n const deps = await plugin.extract(manifestPath, this.projectRoot);\n allDeps.push(...deps);\n manifestsParsed++;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ file: relManifest, message });\n }\n } else {\n // Fallback: use architecture parser output (names only, no versions)\n try {\n const packages = await archParser.parse(manifestPath, this.projectRoot);\n const deps = this._fallbackExtract(packages, archParser.name, relManifest);\n allDeps.push(...deps);\n manifestsParsed++;\n\n if (!plugin) {\n warnings.push({\n file: relManifest,\n message: `No version extraction plugin registered for ecosystem '${archParser.name}'; dependency names extracted without version constraints`,\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ file: relManifest, message });\n }\n }\n }\n\n return {\n dependenciesCreated: allDeps.length,\n manifestsParsed,\n warnings,\n errors,\n };\n }\n\n /**\n * Extract dependencies for a single manifest file.\n * Tries standalone plugins first (when there is no arch parser), then\n * wrapped plugins, then falls back to arch parser output.\n */\n async extractFromManifest(manifestPath: string): Promise<ParsedDependency[]> {\n const archParser = this.archParsers.find(p => p.canParse(manifestPath));\n\n if (!archParser) {\n // No arch parser \u2014 try standalone plugins\n const standalonePlugin = [...this.standalonePlugins.values()]\n .find(p => p.canExtract(manifestPath));\n if (standalonePlugin) {\n return standalonePlugin.extract(manifestPath, this.projectRoot);\n }\n return [];\n }\n\n const plugin = this.plugins.get(archParser.name);\n\n if (plugin && plugin.canExtract(manifestPath)) {\n return plugin.extract(manifestPath, this.projectRoot);\n }\n\n // Fallback: architecture parser output (names only)\n const packages = await archParser.parse(manifestPath, this.projectRoot);\n const relManifest = path.relative(this.projectRoot, manifestPath).replace(/\\\\/g, '/');\n return this._fallbackExtract(packages, archParser.name, relManifest);\n }\n\n /**\n * Get the ecosystem name for a given manifest file path.\n * Returns undefined if no parser handles this file.\n */\n getEcosystem(manifestPath: string): string | undefined {\n const parser = this.archParsers.find(p => p.canParse(manifestPath));\n return parser?.name;\n }\n\n // \u2500\u2500 Private helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Fallback extraction: converts ArchPackage.externalDeps (names only) into\n * ParsedDependency records with empty version constraints and default scope.\n */\n private _fallbackExtract(\n packages: ArchPackage[],\n ecosystem: string,\n sourceManifest: string,\n ): ParsedDependency[] {\n const deps: ParsedDependency[] = [];\n for (const pkg of packages) {\n if (!pkg.externalDeps) continue;\n for (const depName of pkg.externalDeps) {\n deps.push({\n name: depName,\n declaredConstraint: '*',\n scope: 'production',\n ecosystem,\n sourceManifest,\n });\n }\n }\n return deps;\n }\n\n /**\n * Walk the project directory tree looking for manifest files.\n * Mirrors the architecture module's _findManifests() logic, respecting SKIP_DIRS.\n * Includes manifest filenames from standalone plugins in addition to arch parsers.\n */\n private _findManifests(dir: string, results: string[] = []): string[] {\n const manifestFilenames = new Set([\n ...this.archParsers.flatMap(p => p.manifestFiles),\n ...Array.from(this.standalonePlugins.values()).flatMap(p => p.manifestFiles),\n ]);\n\n return this._walkDir(dir, manifestFilenames, results);\n }\n\n private _walkDir(dir: string, manifestFilenames: Set<string>, results: string[]): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name)) {\n this._walkDir(path.join(dir, entry.name), manifestFilenames, results);\n }\n } else if (entry.isFile()) {\n if (manifestFilenames.has(entry.name) || entry.name.endsWith('.csproj')) {\n results.push(path.join(dir, entry.name));\n }\n }\n }\n\n return results;\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["/**\n * SecurityManifestAdapter\n *\n * Wraps the existing architecture manifest parsers (11 parsers: npm, cargo, go,\n * gradle, maven, csproj, python, scala, elm, ocaml) to additionally extract\n * version constraints and dependency scopes that the architecture module does\n * not capture.\n *\n * The architecture parsers discover manifest files and extract package names into\n * `ArchPackage.externalDeps`. This adapter reuses that discovery infrastructure\n * and layers version/scope extraction on top via ecosystem-specific plugins.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { getAllManifestParsers } from '../../architecture/manifest/index';\nimport type { ManifestParser as ArchManifestParser, ArchPackage } from '../../architecture/types';\nimport type { ParsedDependency, ManifestParseResult } from '../types';\n\n// \u2500\u2500 Include/Exclude filter support \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ManifestScanOptions {\n /** Glob patterns for paths to include (relative to projectRoot). Empty = all. */\n include?: string[];\n /** Glob patterns for paths to exclude (relative to projectRoot). */\n exclude?: string[];\n}\n\n// \u2500\u2500 Plugin Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * A version extraction plugin provides ecosystem-specific logic to extract\n * version constraints and scopes from manifest files. Each plugin corresponds\n * to one or more architecture parsers and enhances their output with security-\n * relevant metadata.\n */\nexport interface VersionExtractionPlugin {\n /** Ecosystem identifier matching the architecture parser name (e.g. 'npm', 'maven', 'go') */\n ecosystem: string;\n /** Manifest filenames this plugin handles (should match the architecture parser's manifestFiles) */\n manifestFiles: string[];\n /** Returns true if this plugin can handle the given manifest file path */\n canExtract(manifestPath: string): boolean;\n /**\n * Extract version constraints and scopes from a manifest file.\n * This is called AFTER the architecture parser has already discovered the manifest.\n * The plugin receives the manifest path and project root, and returns detailed\n * dependency information including versions and scopes.\n */\n extract(manifestPath: string, projectRoot: string): Promise<ParsedDependency[]>;\n}\n\n// \u2500\u2500 Directories to skip (mirrors architecture module's SKIP_DIRS) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst SKIP_DIRS = new Set([\n 'node_modules', '.git', 'dist', 'build', 'target', '.gradle', '__pycache__',\n '.kirograph', 'vendor', '.cache', 'coverage', '.nyc_output', '_build', '_opam',\n 'elm-stuff', 'zig-cache', 'zig-out',\n]);\n\n// \u2500\u2500 SecurityManifestAdapter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class SecurityManifestAdapter {\n private readonly archParsers: ArchManifestParser[];\n private readonly plugins: Map<string, VersionExtractionPlugin> = new Map();\n /** Standalone plugins have no corresponding architecture parser \u2014 they own their own discovery. */\n private readonly standalonePlugins: Map<string, VersionExtractionPlugin> = new Map();\n private readonly projectRoot: string;\n private readonly scanOptions: ManifestScanOptions;\n\n constructor(projectRoot: string, scanOptions?: ManifestScanOptions) {\n this.projectRoot = projectRoot;\n this.scanOptions = scanOptions ?? {};\n this.archParsers = getAllManifestParsers();\n }\n\n /**\n * Register a version extraction plugin for an ecosystem that has a\n * corresponding architecture parser. The plugin's `ecosystem` field\n * must equal the architecture parser's `name` for the lookup to work.\n */\n registerPlugin(plugin: VersionExtractionPlugin): void {\n this.plugins.set(plugin.ecosystem, plugin);\n }\n\n /**\n * Register a standalone version extraction plugin for an ecosystem that\n * has NO corresponding architecture parser. The plugin's `manifestFiles`\n * list drives discovery \u2014 the adapter will scan for those filenames and\n * invoke the plugin directly when found.\n */\n registerStandalonePlugin(plugin: VersionExtractionPlugin): void {\n this.standalonePlugins.set(plugin.ecosystem, plugin);\n }\n\n /**\n * Get all registered version extraction plugins (wrapped + standalone).\n */\n getPlugins(): VersionExtractionPlugin[] {\n return [...this.plugins.values(), ...this.standalonePlugins.values()];\n }\n\n /**\n * Get the architecture parsers being wrapped.\n */\n getArchParsers(): ArchManifestParser[] {\n return this.archParsers;\n }\n\n /**\n * Discover all manifest files in the project tree.\n * Includes files known to architecture parsers AND files from standalone plugins.\n * Respects config include/exclude patterns when provided.\n * Returns absolute paths.\n */\n discoverManifests(): string[] {\n const allManifests = this._findManifests(this.projectRoot);\n\n // If no include patterns, return all discovered manifests\n if (!this.scanOptions.include || this.scanOptions.include.length === 0) {\n return allManifests;\n }\n\n // Filter manifests to only those within included paths\n const picomatch = require('picomatch');\n const includeMatchers = this.scanOptions.include.map((p: string) => picomatch(p));\n const excludeMatchers = (this.scanOptions.exclude ?? []).map((p: string) => picomatch(p));\n\n return allManifests.filter(absPath => {\n const rel = path.relative(this.projectRoot, absPath).replace(/\\\\/g, '/');\n const included = includeMatchers.some((m: (s: string) => boolean) => m(rel));\n if (!included) return false;\n const excluded = excludeMatchers.some((m: (s: string) => boolean) => m(rel));\n return !excluded;\n });\n }\n\n /**\n * Run full discovery and extraction:\n * 1. Discover manifest files (reusing architecture's SKIP_DIRS logic)\n * 2. For each manifest, find the matching architecture parser\n * 3. If a version extraction plugin is registered for that ecosystem,\n * use it to extract detailed version/scope information\n * 4. Otherwise, fall back to basic extraction from the architecture parser's\n * ArchPackage.externalDeps (names only, no version info)\n */\n async extractAll(): Promise<ManifestParseResult> {\n const manifestPaths = this.discoverManifests();\n const allDeps: ParsedDependency[] = [];\n const warnings: Array<{ file: string; line?: number; message: string }> = [];\n const errors: Array<{ file: string; message: string }> = [];\n let manifestsParsed = 0;\n\n for (const manifestPath of manifestPaths) {\n const archParser = this.archParsers.find(p => p.canParse(manifestPath));\n if (!archParser) continue;\n\n const relManifest = path.relative(this.projectRoot, manifestPath).replace(/\\\\/g, '/');\n const plugin = this.plugins.get(archParser.name);\n\n const standalonePlugin = (!plugin || !plugin.canExtract(manifestPath))\n ? [...this.standalonePlugins.values()].find(p => p.canExtract(manifestPath))\n : undefined;\n\n if (plugin && plugin.canExtract(manifestPath)) {\n // Use the version extraction plugin for detailed parsing\n try {\n const deps = await plugin.extract(manifestPath, this.projectRoot);\n allDeps.push(...deps);\n manifestsParsed++;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ file: relManifest, message });\n }\n } else if (standalonePlugin) {\n // A wrapped plugin exists for this arch parser's ecosystem but\n // doesn't claim this specific file \u2014 e.g. pip's plugin only claims\n // requirements.txt, not pyproject.toml, which the Python arch parser\n // also matches. Use the standalone plugin registered for it instead.\n try {\n const deps = await standalonePlugin.extract(manifestPath, this.projectRoot);\n allDeps.push(...deps);\n manifestsParsed++;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ file: relManifest, message });\n }\n } else {\n // Fallback: use architecture parser output (names only, no versions)\n try {\n const packages = await archParser.parse(manifestPath, this.projectRoot);\n const deps = this._fallbackExtract(packages, archParser.name, relManifest);\n allDeps.push(...deps);\n manifestsParsed++;\n\n if (!plugin) {\n warnings.push({\n file: relManifest,\n message: `No version extraction plugin registered for ecosystem '${archParser.name}'; dependency names extracted without version constraints`,\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ file: relManifest, message });\n }\n }\n }\n\n return {\n dependenciesCreated: allDeps.length,\n manifestsParsed,\n warnings,\n errors,\n };\n }\n\n /**\n * Extract dependencies for a single manifest file.\n * Tries standalone plugins first (when there is no arch parser), then\n * wrapped plugins, then falls back to arch parser output.\n */\n async extractFromManifest(manifestPath: string): Promise<ParsedDependency[]> {\n const archParser = this.archParsers.find(p => p.canParse(manifestPath));\n\n if (!archParser) {\n // No arch parser \u2014 try standalone plugins\n const standalonePlugin = [...this.standalonePlugins.values()]\n .find(p => p.canExtract(manifestPath));\n if (standalonePlugin) {\n return standalonePlugin.extract(manifestPath, this.projectRoot);\n }\n return [];\n }\n\n const plugin = this.plugins.get(archParser.name);\n\n if (plugin && plugin.canExtract(manifestPath)) {\n return plugin.extract(manifestPath, this.projectRoot);\n }\n\n // A wrapped plugin can be registered under the same key as the arch\n // parser without claiming this specific file \u2014 e.g. pip's plugin only\n // claims requirements.txt, but the Python arch parser also matches\n // pyproject.toml. Check standalone plugins before giving up on version\n // info entirely.\n const standalonePlugin = [...this.standalonePlugins.values()]\n .find(p => p.canExtract(manifestPath));\n if (standalonePlugin) {\n return standalonePlugin.extract(manifestPath, this.projectRoot);\n }\n\n // Fallback: architecture parser output (names only)\n const packages = await archParser.parse(manifestPath, this.projectRoot);\n const relManifest = path.relative(this.projectRoot, manifestPath).replace(/\\\\/g, '/');\n return this._fallbackExtract(packages, archParser.name, relManifest);\n }\n\n /**\n * Get the ecosystem name for a given manifest file path.\n * Returns undefined if no parser handles this file.\n */\n getEcosystem(manifestPath: string): string | undefined {\n const parser = this.archParsers.find(p => p.canParse(manifestPath));\n return parser?.name;\n }\n\n // \u2500\u2500 Private helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Fallback extraction: converts ArchPackage.externalDeps (names only) into\n * ParsedDependency records with empty version constraints and default scope.\n */\n private _fallbackExtract(\n packages: ArchPackage[],\n ecosystem: string,\n sourceManifest: string,\n ): ParsedDependency[] {\n const deps: ParsedDependency[] = [];\n for (const pkg of packages) {\n if (!pkg.externalDeps) continue;\n for (const depName of pkg.externalDeps) {\n deps.push({\n name: depName,\n declaredConstraint: '*',\n scope: 'production',\n ecosystem,\n sourceManifest,\n });\n }\n }\n return deps;\n }\n\n /**\n * Walk the project directory tree looking for manifest files.\n * Mirrors the architecture module's _findManifests() logic, respecting SKIP_DIRS.\n * Includes manifest filenames from standalone plugins in addition to arch parsers.\n */\n private _findManifests(dir: string, results: string[] = []): string[] {\n const manifestFilenames = new Set([\n ...this.archParsers.flatMap(p => p.manifestFiles),\n ...Array.from(this.standalonePlugins.values()).flatMap(p => p.manifestFiles),\n ]);\n\n return this._walkDir(dir, manifestFilenames, results);\n }\n\n private _walkDir(dir: string, manifestFilenames: Set<string>, results: string[]): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name)) {\n this._walkDir(path.join(dir, entry.name), manifestFilenames, results);\n }\n } else if (entry.isFile()) {\n if (manifestFilenames.has(entry.name) || entry.name.endsWith('.csproj')) {\n results.push(path.join(dir, entry.name));\n }\n }\n }\n\n return results;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["standalonePlugin"]
|
|
7
7
|
}
|
|
@@ -67,8 +67,31 @@ async function parseCargoManifest(manifestPath, projectRoot) {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
const rootCrateName = extractCargoPackageName(content);
|
|
71
|
+
const directNames = new Set(dependencies.map((d) => d.name));
|
|
72
|
+
for (const [name, version] of resolvedVersions) {
|
|
73
|
+
if (directNames.has(name) || name === rootCrateName) continue;
|
|
74
|
+
dependencies.push({
|
|
75
|
+
name,
|
|
76
|
+
declaredConstraint: version,
|
|
77
|
+
resolvedVersion: version,
|
|
78
|
+
scope: "production",
|
|
79
|
+
ecosystem: "cargo",
|
|
80
|
+
sourceManifest: relativeManifest
|
|
81
|
+
});
|
|
82
|
+
}
|
|
70
83
|
return dependencies;
|
|
71
84
|
}
|
|
85
|
+
function extractCargoPackageName(content) {
|
|
86
|
+
const packageSectionMatch = /^\[package\]\s*$/m.exec(content);
|
|
87
|
+
if (!packageSectionMatch) return void 0;
|
|
88
|
+
const startIdx = packageSectionMatch.index + packageSectionMatch[0].length;
|
|
89
|
+
const remaining = content.slice(startIdx);
|
|
90
|
+
const nextSection = remaining.match(/^\[/m);
|
|
91
|
+
const sectionContent = nextSection ? remaining.slice(0, nextSection.index) : remaining;
|
|
92
|
+
const nameMatch = sectionContent.match(/^name\s*=\s*"([^"]+)"/m);
|
|
93
|
+
return nameMatch ? nameMatch[1].trim() : void 0;
|
|
94
|
+
}
|
|
72
95
|
function extractSectionDependencies(content, section, relativeManifest) {
|
|
73
96
|
const deps = [];
|
|
74
97
|
const sectionRegex = new RegExp(`^\\[${escapeRegex(section)}\\]\\s*$`, "m");
|